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).

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

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

Давайте теперь выделим особенности этого подхода к программированию:

1. Код хорошо структурирован;
2. Упрощается контроль кода, так как в алгоритме можно выделить отдельные состояния, связанные с переключениями выходных сигналов на графике;
3. Благодаря двум предыдущим пунктам снижается вероятность появления логических ошибок, ошибку в коде проще локализовать;
4. Время выполнения однопроходной функции более предсказуемо, но ограничено необходимой точностью к входным и выходным воздействиям;
5. Достаточно просто организовать параллельность обработки данных;
6. Не допускается использование задержек времени внутри однопроходной функции, кроме функций, реализующий базовый интервал времени;
7. Необходимо крайне аккуратно использовать операторы цикла, так как они потенциально могут увеличить время выполнения функции и выйти за границы базового интервала;
8. Объем программы как правило превышает аналогичный по функциональности линейный алгоритм;
9. Процесс разработки и отладки программы более трудоемкий и требует адаптации мышления;

image

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

Использование флагов для управления однопроходной программой


Использование флагов в программировании имеет глубокие ассемблерные корни. Такие абстракции, как Arduino или HAL-драйвер под STM32, для работы с периферией микроконтроллера используют готовые библиотеки с удобными интерфейсами. В ассемблерных программах для этих же целей приходилось напрямую манипулировать отдельными разрядами управляющих регистров в памяти микроконтроллера. Биты в этих регистрах ввода/вывода по сути являются флагами состояния периферийных устройств.

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

imageРегистр флагов чем-то похож на мишень для биатлона. Событие — это попадание в мишень, которое отмечается поднятием флажка.

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

Современные микроконтроллеры, такие как STM32, имеют колоссальные объемы оперативной и программной памяти. Это позволяет применять для их программирования объектно-ориентированную парадигму. Но компактные 8-ми битные устройства все равно еще имеют широкое применение. Хотя уровень оптимизации современных Си компиляторов позволяет обойтись без ассемблера и для них, сами подходы к программированию заставляют использовать ассемблерные приемы.

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

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

image

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

image

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

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

image

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

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

Для обработки кнопки воспользуюсь кодом, который я приводил в своей статье «Неблокирующая обработка тактовой кнопки для Arduino.». Для этого в папке с проектом обработчика светофора нужно разместить файлы «myNonblockingButton.h» и «myNonblockingButton.cpp» из этой статьи.

Чтобы воспользоваться библиотекой «myNonblockingButton.h», в начале программы необходимо подключить файл «myNonblockingButton.h» с помощью директивы "#include" и объявить экземпляр структуры типа «ButtonConfiguration», в полях которой настроить параметры кнопки.

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

Проект для Arduino будет состоять из трех текстовых файлов:

1. sketch.ino
//--------------------------------------------------
//библиотека для обработки кнопки
#include "myNonblockingButton.h"

//--------------------------------------------------
//Порт для обработки кнопки
#define PIN_BUTTON 7

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

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

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

//--------------------------------------------------
//параметры кнопки
struct ButtonConfiguration button = { 
  .pressIdentifier      = button_SB1_Press,
  .pressIdentifierShort = button_SB1_shortPress,
  .pressIdentifierLong  = button_SB1_longPress,
  .pin                  = PIN_BUTTON,
};

//--------------------------------------------------
//структура для хранения параметров светофора
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;

//хранение параметров светофора
TrafficLight_t trafficLight = {
  .portRed    = PORT_RED,
  .portYellow = PORT_YELLOW,
  .portGreen  = PORT_GREEN,

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

//--------------------------------------------------
//для измерения времени работы светофора
uint16_t counter;

//--------------------------------------------------
//состояния светофора
typedef enum {TF_stop, TF_run} TF_State_t;

//флаг состояния светофора
TF_State_t TF_Flaf = TF_run;

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

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

//--------------------------------------------------
//супер цикл
void loop() {
    //Обработка светофора
    TF_Flaf = trafficLight_action(&trafficLight, counter);

    //устанавливаем флаг состояния светофора, если кнопка нажата
    if(buttonProcessing(&button, TICK) == button_SB1_Press)
      TF_Flaf = TF_run;

    //обработка счетчика времени
    if(TF_Flaf == TF_run){
      counter += TICK;

      if(counter >= TIME_RED + TIME_GREEN + 6*TIME_PULS + TIME_YELLOW2)
        counter = 0; 
    }

    //формирование базового интервала времени
    delay(TICK);  
}

//--------------------------------------------------
//обработчик светофора
TF_State_t trafficLight_action(TrafficLight_t * tf, uint16_t counter){
  //для возврата состояния светофора
  TF_State_t tempFlag = TF_run;

  //циклический перенос счетчика
  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){
    tempFlag = TF_stop;
    digitalWrite(tf->portRed,    LOW);
    digitalWrite(tf->portYellow, LOW);
    digitalWrite(tf->portGreen,  HIGH);
  }
  else
  //включен зеленый
  if( (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);
  }

  return tempFlag;
}
2. myNonblockingButton.h
#ifndef __myNonblockingButton_h
#define __myNonblockingButton_h

//--------------------------------------------------
//время дребезга кнопки
#define BUTTON_PRESS_TIME 50
//время короткого нажатия
#define BUTTON_SHORT_PRESS_TIME 100
//время длинного нажатия
#define BUTTON_LONG_PRESS_TIME 1000
//максимально возможное время нажатия
#define MAX_PRESS_DURATION BUTTON_LONG_PRESS_TIME

//--------------------------------------------------
//физическое состояние кнопки
enum ButtonResult {
  buttonNotPress,       //если кнопка не нажата
  button_SB1_Press,     //код нажатия кнопки SB1
  button_SB1_shortPress,//код короткого нажатия
  button_SB1_longPress  //код длинног нажатия
};

struct ButtonConfiguration {
  //код кнопки при нажатии
  enum ButtonResult pressIdentifier;
  //код кнопки при нажатии
  enum ButtonResult pressIdentifierShort;
  //код кнопки при нажатии
  enum ButtonResult pressIdentifierLong;
  //номер входа, к которому подключена кнопка
  uint8_t pin;
  //флаг первого срабатывания кнопки
  bool clickFlag;
  //для измерения длительности нажатия
  uint16_t pressingTime;
};

//--------------------------------------------------
//настройка входа для кнопки
void buttonInit(struct ButtonConfiguration* button);
//обработчик кнопки
enum ButtonResult buttonProcessing(struct ButtonConfiguration* button, uint16_t time);

#endif
3. myNonblockingButton.cpp
//--------------------------------------------------
//потому что надо
#include <Arduino.h>
//подключение библиотеки с нашей кнопкой
#include "myNonblockingButton.h"

//--------------------------------------------------
//настройка входа для кнопки с подтяжкой
void buttonInit(struct ButtonConfiguration* button){
  pinMode(button->pin, INPUT_PULLUP);
}

//--------------------------------------------------
//обработчик кнопки
enum ButtonResult buttonProcessing(struct ButtonConfiguration* button, uint16_t time){
  //для временного хранения кода нажатия кнопки
  enum ButtonResult temp = buttonNotPress;

  //если кнопка нажата
  if(digitalRead(button->pin) == LOW){
    //считаем время нажатия
    button->pressingTime += time;

    //защита от переполнения
    if(button->pressingTime >= MAX_PRESS_DURATION){
      button->pressingTime = MAX_PRESS_DURATION;
    }
    
    //проверка дребезга
    if(button->pressingTime >= BUTTON_PRESS_TIME && button->clickFlag == false){
      temp = button->pressIdentifier;
      button->clickFlag = true;
    }
  }
  //если кнопка не нажата
  else{
    //проверяем, сколько времени продолжалось нажатие кнопки
    if(button->pressingTime >= BUTTON_LONG_PRESS_TIME){
      temp = button->pressIdentifierLong;
    }
    else if(button->pressingTime >= BUTTON_SHORT_PRESS_TIME){
      temp = button->pressIdentifierShort;
    }

    //сбрасываем для следующего измерения
    button->pressingTime = 0;
    button->clickFlag = false;
  }

  //возвращаем результат обработки кнопки
  return temp;
}

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

Заключение


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

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

Однопроходные функции также не являются панацеей для программирования микроконтроллеров, но позволяет решать широкий круг задач с «умеренной» сложностью. Хорошо подходит данный метод для микроконтроллеров типа AVR tiny, PIC или STM8. Много кода подобным образом было написано для микроконтроллеров на MSC51.

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

Вы можете посмотреть другие мои статьи на тему программирования Arduino:

1. Тактовая кнопка, как подключить правильно к "+" или "-"
2. Экономим выводы для Arduino. Управление сдвиговым регистром 74HC595 по одному проводу
3. Блокирующая обработка тактовой кнопки для Arduino. Настолько полный гайд, что ты устанешь его читать
4. Неблокирующая обработка тактовой кнопки для Arduino. Как использовать прерывание таймера «в два клика» в стиле ардуино
5. С чем едят конечный автомат

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


Комментарии (33)


  1. fk0
    00.00.0000 00:00
    +10

    Есть так называемая технология автоматного программирования или проектирования. В частности способ програмирования конечных автоматов в языке C -- "Switch-технология" А. А. Шалыто.

    Рекомендую к ознакомлению: http://softcraft.ru/auto/ и https://is.ifmo.ru/automata/

    Там поднятые автором статьи вопросы в значительной степени давно разобраны. Обработка конечным автоматом входных событий в своём одном состоянии -- и есть "однопроходная функция".

    Немного отступая, стоит ещё вспомнить язык "Дракон". Где отдельные "шампуры" -- работа программы в одном состоянии. И проход блок-схемы "шампура" та же "однопроходная функция".

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

    Важным является ещё не только то, как запрограммировать отдельный автомат, но как построить управляющую систему в целом, состоящую из множества конечных автоматов. В общем случае можно строить иерархические системы по технологии Шалыто, или работать с автоматами выполняемыми условно параллельно (на самом деле последовательно, поочерёдно обрабатывая поступающие события) под управлением некоторого "планировщика". Взаимодействие может осуществляться путём наблюдения за состоянием другого автомата (Шалыто) или через обмен сообщениями (событиями).

    Простейшим способом планирования выполнения автоматов может быть очередь сообщений: сообщение из головы очереди отправляется последовательно на обработку всем автоматам, которым оно предназначено (или которые его обрабатывают). Такой способ применён в системе Quantum Leaps (теперь state-machines.com). Данный способ имеет очевидный недостаток: подразумевается, что автомат в процессе обработки события может сам породить множество событий предназначенных другим автоматам. И такое поведение может привести к "лавине событий", переполняющей очередь сообщений.

    Можно принять, что события являются атомарными: событие или произошло в прошлом и ещё не обработано, и не важно сколько раз, или событие не возникало. Событие не несёт никакой информации. Информация ассоциированная с событием, если есть, должна передаваться отдельно (через выделенные FIFO-очереди и т.п.) И очередь сообщений скорей должна превратиться в очередь с приоритетом, где очередное событие размещается в очереди только в случае если оно уже там не размещено. Тогда переполнение очереди невозможно, но происходит некоторое усложнение логики, т.к. события говорят только о факте своего возникновения, но ни не несут ассоциированную информацию, ни даже не говорят сколько раз они произошли. В принципе, всё это становится похожим на libasync или boost.asio...

    Рекомендую к чтению "Механизм обмена сообщениями для параллельно
    работающих автоматов (на примере системы управления турникетом): https://is.ifmo.ru/download/turn.pdf

    В принципе, автоматное программирование является в какой-то мере альтернативой традиционномым способам программирования многозадачных систем. Как правило автоматная программа -- это большое множество параллельных процессов, где "переключение контекстов" осуществляется после обработки очередного события очередным автоматом (а сама обработка события -- не вытесняема). Таким образбом отпадает нужда в механизмах синхронизации (и масса связанных с этим ошибок), отпадает нужда в выделении памяти под стек каждого отдельного потока, нет нужды в долгом переключении контекстов. Но есть и обратная сторона медали: реактивность автоматной системы может оказаться ниже, т.к. определяется временем обработки события самого медленного автомата. А ещё может и длиной очереди сообщений, впрочем и в традиционной многозадачной системе планировщик может иметь очередь готовых к исполнению процессов не нулевой длины.

    Ещё нужно отметить, что распространённая в embedded-среде методика программирования с использованием т.н. Big Loop -- по сути тот же планировщик запускающий отдельные автоматы. Но крайне не эффективный: ни только нет приоритетов, так ещё и автоматы запускаются независимо от того, есть ли для них входные события или нет. Когда автоматов (задач) становится очень много, цикл начинает проворачиваться неприемлемо медленно.

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


    1. OldFashionedEngineer Автор
      00.00.0000 00:00

      С работой Шалыто я знаком. К стати, он есть в ВК, я с ним как-то имел наглость проконсультироваться.

      С автоматным программированием я хорошо знаком но не все же сразу.


    1. OldFashionedEngineer Автор
      00.00.0000 00:00

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

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


  1. rutexd
    00.00.0000 00:00
    +1

    А почему не использовать например методологию grafcet?

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


    1. OldFashionedEngineer Автор
      00.00.0000 00:00

      Спасибо за комментарий. Методик очень много разных существует. Чтобы дойти до некоторых, нужно пройти определённый путь. Я решил начать рассказ с самого начала: флаги и однопроходные функции. Про диаграммы в следующий раз остановлюсь подробнее.


      1. rutexd
        00.00.0000 00:00
        +1

        Спасибо к слову за статью. Узнал для себя некоторые интересные моменты. Поставил бы везде плюсы, да карма хабра - зло. Аффор, пиши есчё.


        1. OldFashionedEngineer Автор
          00.00.0000 00:00

          Хороший комментарий лучше всяких там плюсов.


  1. Indemsys
    00.00.0000 00:00
    +4

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

    Сначала создаётся тип управляющей структуры для отдельной машины состояний и массив таких структур по количеству необходимых лампочек:

    // Управляющая структура машины состояний управляемой шаблоном
    typedef struct
    {
        uint8_t          active;
        T_sys_timestump  last_timestump;      // Время последнего вызова автомата состояний
        uint32_t         state_duration;      // Длительность состояний в мс
        int32_t          *start_pattern_ptr;  // Указатель на массив констант являющийся цепочкой состояний (шаблоном)
                                              // Если значение в массиве = 0xFFFFFFFF, то процесс обработки завершается
                                              // Если значение в массиве = 0x00000000, то вернуть указатель на начало цепочки
        int32_t          *curr_pattern_ptr;   // Текущая позиция в цепочке состояний
        uint8_t          repeat_flag;         // Флаг принудительного повторения сигнала
    
    } T_state_machine_control_block;
    
    T_state_machine_control_block outs_cbl[OUTPUTS_NUM];
    

    Потом создаётся функция запуска машины состояний для каждой отдельной лампочки

    
    /*-----------------------------------------------------------------------------------------------------
      Инициализация шаблона для машины состояний выходного сигнала
    
      \param pttn    - указатель на запись шаблоне
      \param n       - номер сигнала
    -----------------------------------------------------------------------------------------------------*/
    void Set_output_pattern(const int32_t *pttn, uint32_t n)
    {
      if (n >= OUTPUTS_NUM) return;
      if (pttn != 0)
      {
        if (outs_cbl[n].start_pattern_ptr != (int32_t *)pttn)
        {
          outs_cbl[n].start_pattern_ptr = (int32_t *)pttn;
          outs_cbl[n].curr_pattern_ptr = (int32_t *)pttn;
          Set_output_state(n,*outs_cbl[n].curr_pattern_ptr);
          outs_cbl[n].curr_pattern_ptr++;
          Get_hw_timestump(&outs_cbl[n].last_timestump);
          outs_cbl[n].curr_pattern_ptr++;
          outs_cbl[n].active = 1;
        }
        else
        {
          outs_cbl[n].repeat_flag = 1;
        }
      }
    }

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

    /*-----------------------------------------------------------------------------------------------------
       Автомат состояний выходных сигналов
    
      \param tnow  - текущее машинное время в тактах процессора
    -----------------------------------------------------------------------------------------------------*/
    void Outputs_state_automat(T_sys_timestump *tnow)
    {
      uint32_t         duration;
      uint32_t         output_state;
    
      for (uint32_t n = 0; n < OUTPUTS_NUM; n++)
      {
        if (outs_cbl[n].active) // Отрабатываем шаблон только если активное состояние
        {
          uint32_t dt = Timestump_diff_to_msec(&outs_cbl[n].last_timestump  , tnow);
    
          if (dt >= outs_cbl[n].state_duration)  // Меняем состояние сигнала при обнулении счетчика
          {
            memcpy(&outs_cbl[n].last_timestump, tnow, sizeof(T_sys_timestump));
    
            if (outs_cbl[n].start_pattern_ptr != 0)  // Проверяем есть ли назначенный шаблон
            {
              output_state =*outs_cbl[n].curr_pattern_ptr;   // Выборка значения состояния выхода
              outs_cbl[n].curr_pattern_ptr++;
              duration =*outs_cbl[n].curr_pattern_ptr;       // Выборка длительности состояния
              outs_cbl[n].curr_pattern_ptr++;                // Переход на следующий элемент шаблона
              if (duration != 0xFFFFFFFF)
              {
                if (duration == 0)  // Длительность равная 0 означает возврат указателя элемента на начало шаблона и повторную выборку
                {
                  outs_cbl[n].curr_pattern_ptr = outs_cbl[n].start_pattern_ptr;
                  output_state =*outs_cbl[n].curr_pattern_ptr;
                  outs_cbl[n].curr_pattern_ptr++;
                  outs_cbl[n].state_duration =*outs_cbl[n].curr_pattern_ptr;
                  outs_cbl[n].curr_pattern_ptr++;
                  Set_output_state(n , output_state);
                }
                else
                {
                  outs_cbl[n].state_duration = duration;
                  Set_output_state(n ,output_state);
                }
              }
              else
              {
                if (outs_cbl[n].repeat_flag)
                {
                  outs_cbl[n].repeat_flag = 0;
                  // Возврат указателя элемента на начало шаблона и повторная выборка
                  outs_cbl[n].curr_pattern_ptr = outs_cbl[n].start_pattern_ptr;
                  output_state =*outs_cbl[n].curr_pattern_ptr;
                  outs_cbl[n].curr_pattern_ptr++;
                  outs_cbl[n].state_duration =*outs_cbl[n].curr_pattern_ptr;
                  outs_cbl[n].curr_pattern_ptr++;
                  Set_output_state(n , output_state);
                }
                else
                {
                  // Обнуляем счетчик и таким образом выключаем обработку паттерна
                  Set_output_state(n , output_state);
                  outs_cbl[n].active = 0;
                  outs_cbl[n].start_pattern_ptr = 0;
                }
              }
            }
            else
            {
              // Если нет шаблона обнуляем состояние выходного сигнала
              Set_output_state(n, 0);
            }
          }
        }
      }
    }
    

    И наконец создаём какие угодно шаблоны для мигания лампочек

    #define __ON   0 // Ноль потому что лампочки зажигаются низким уровнем сигнала 
    #define _OFF   1
    #define _LOOP  0
    #define _STOP  0xFFFFFFFF
    #define    SC  1 // Масштабирующий коэффициент 
                     // если понадобится замедлить или ускорить 
                     // все моргания одновременно
    
    //-------------------------------------------------------------------------
    // Пример шаблона для периодического тройного помаргивания
    
    //  Шаблон состоит из массива груп слов.
    //  Первое слово в группе - значение сигнала
    //  Второе слово в группе - длительность интервала времени в  мс
    //    интервал равный 0x00000000 - означает возврат в начало шаблона
    //    интервал равный 0xFFFFFFFF - означает застывание состояния
    
    const int32_t   OUT_3_BLINK[] =
    {
      __ON, 50*SC,
      _OFF, 50*SC,
      __ON, 50*SC,
      _OFF, 50*SC,
      __ON, 50*SC,
      _OFF, 250*SC,
      _OFF, _LOOP
    };
    
    
    // Пример включения сигнала с заданным шаблоном тройного моргания
    // В данном примере сигнал зациклиться и будет 
    // продолжаться пока его не отключат сбросив флаг  outs_cbl[LAMP1].active = 0;
    Set_output_pattern(OUT_3_BLINK, LAMP1);

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

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


    1. OldFashionedEngineer Автор
      00.00.0000 00:00

      Вот представьте себе ситуацию. Вы только начали вникать в процесс программирования. А тут такое выкатилось. Очень сложно сходу въехать. Надо к этому подбираться постепенно.

      Ко мне недавно обратился знакомый. Попросил проекты для примера. Он когда-то давно учился меня программированию. А потом долго этим не занимался. Я скинул ему свою версию обработчик событий в автоматом стиле. Объяснял пол дня что да как. В итоге он все равно попросил пример по проще.

      Ещё одна важная вещь - это уловить концепцию методики проектирования. Очень хорошо, когда вы в голове можете сложить структуру конечного автомата. Но сколько вы к этому шли?

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


      1. Indemsys
        00.00.0000 00:00
        +1

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

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

        Поэтому я вношу фичу - неограниченное количество сигналов, нечувствительность к таймингам (эту проблему все новички знают сразу), способ лаконичного описания сигналов.


        1. OldFashionedEngineer Автор
          00.00.0000 00:00

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

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

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

          Я ни кого не водил за нос. В самом начале статьи чётко обозначено, кому она адресована.


          1. Indemsys
            00.00.0000 00:00
            +1

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

            Дозирование фичей должно быть конечно. Не больше 7. У меня их три. В самый раз.

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

            Словом, правильно ли вы представляете целевую аудиторию своей статьи?


            1. OldFashionedEngineer Автор
              00.00.0000 00:00

              Скорее всего вы статью не читали, а просто пролистали по диагонали. И это не удивительно.

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

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

              Вот это вы вообще к чему сейчас? В моем примере параметры хранятся в глобальной структуре. Переменные счётчик и флаг тоже объявлены глобально.

              Раз так, то давайте конкретно по коду из вашего примера. Покажите мне в нем хотя-бы один конечный автомат?!! Кашу из иф-элсов я увидел, но машину состояний нет.


            1. OldFashionedEngineer Автор
              00.00.0000 00:00

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

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

              Чтобы осмыслить код из вашего примера, нужно обладать определённым знанием синтаксиса и опытом в программировании. А остальным как быть?

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


            1. OldFashionedEngineer Автор
              00.00.0000 00:00

              Словом, правильно ли вы представляете целевую аудиторию своей статьи?

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

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


        1. OldFashionedEngineer Автор
          00.00.0000 00:00
          +2

          Дайте цитату на то место, где я утверждаю, что мой код работает кое-как?

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

          Все должно быть к месту. Иначе получается стрельба из пушки по воробьям.

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


          1. Indemsys
            00.00.0000 00:00

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

            Мой исходник не для того чтобы опровергнуть все ваши утверждения, а просто пример машины состояний. Машины состояний они так и делаются на if-ах. Так генерирует машины например Mаtlab. Машины меняют состояния не по событиям, а по изменениям условий! А события как раз ломают логику машин состояний, вносят плохо воспринимаемую мозгом асинхронность.

            Про глобальные переменные я завёл речь в плане того что вы там пообещали все перевести в классы намекая на C++ видимо и спрятать переменные внутри классов. Это сильно ухудшает отлаживаемость.

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

            А метафоры как раз нужны из области машин состояний (state machine, SM). И вот этот барьер очень сложно перескочить. Нужно структуры создавать для SM, а не для объектов материального мира. Но нужно научиться редуцировать свои задачи на отдельные SM.

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

            Т.е. в следующих статьях я бы вам предложил раскрыть темы:

            • дизайн кода с учётом максимальной отлаживаемости,

            • дизайн кода с учётом удобства рефакторинга,

            • дизайн кода с учётом масштабируемости.

            Думаю что вот это было бы реально интересно новичкам. Про это почему-то в книгах не пишут.


            1. OldFashionedEngineer Автор
              00.00.0000 00:00

              Так генерирует машины например MаMаtlab.н

              Не сравнивайте код, генерируемый автоматически с кодом, который надо руками писать.

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

              Мне кажется, что Вы слишком зациклены на конечных автомата. В моем обработчике кнопки конечного автомата нет.

              В начале статьи я специально написал, что статья не про конечные автоматы. И я предупредил, что статья не для вашего уровня подготовки. Чем я Вас обманул?!

              Как по вашему должна выглядеть хорошая статья? "Пацаны, смотрите, тут такая фича..." и дальше дофига кода.

              Простите, Вя сколько лет программированием занимаетесь? Точно же не первый десяток.


              1. Indemsys
                00.00.0000 00:00

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

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


                1. OldFashionedEngineer Автор
                  00.00.0000 00:00

                  Вы используете термин state machine. Если переводить этот термин на пусский, получается конечный автомат.

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

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

                  Я как раз и хочу выйти к пониманию, что КА это частный случай.


      1. VladimirFarshatov
        00.00.0000 00:00

        Возражу. Не сложно, а порой даже проще чем начинать с линейного программирования. Обучал сына (10-12лет) программированию конечных автоматов, и кстати, как раз на "умном светофоре". К сожалению, он уже знал алгоритмическое программирование на базе Ардублока и Лего скретчей, поэтому пришлось тоже "перестраивать" свое понимание что такое программа.

        Этапы:

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

        2. Из одного состояние в другое лампочка переходит не просто так, а по возникновению "события". Срабатывание таймера, в общем-то такое же событие, как и нажатие на кнопку.

          На базе этих двух пунктов, ребенок в 10-12лет уже способен самостоятельно(!) определить граф состояний конечного автомата умного светофора с кнопкой для пешехода. Проверено на практике.

        3. И вот тут есть тонкий момент, которого нет в "академической" теории КА: входящий поток событий .. "слушается" соответствующим слушателем потока. Сам дошел не сразу, но как только сия мысль сформулировалась примерно в таком акцепте, так сразу дошло и до него КАК происходит работа конечного автомата.

          Простой светофор пошагово описан тут: https://community.alexgyver.ru/threads/programmirovanie-konechnyx-avtomatov-bez-delay.2657 Ещё не знаю, разрешено ли тут постить ссылки на сторонние ресурсы, но .. пусть будет.


        1. OldFashionedEngineer Автор
          00.00.0000 00:00
          +1

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

          Ощущение, что многие комментаторы ни то чтоб не вникают в статью, а даже её не читают полностью. Но зато сразу начинают возражать)))


          1. VladimirFarshatov
            00.00.0000 00:00

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


            1. OldFashionedEngineer Автор
              00.00.0000 00:00

              У Вас имеется большая статистика по применению вашего подхода в обучении?

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

              И переучивать нужно и учить правильно тоже нужно. Именно переучивать, а не ломать. Поэтому я привожу понятные знакомые примеры, а затем показываю альтернативу.

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


  1. sterr
    00.00.0000 00:00

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

    Ну нет! Я понимаю, что это круто, винтажно, и это мы проходили 30 лет назад. Ну если так хотите, то ПЛИС то же самое, но в одном корпусе. Но я сторонник японской простоты и надежности. Я бы реализовал это еще проще. Генератор, счетчик, OTP ROM (осциллограммы то есть). Все просто и ничего лишнего. Конфигурирование - простая замена ROM. 3 секунды. Если хотите с извратами - EEPROM, UVEPROM.

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


    1. OldFashionedEngineer Автор
      00.00.0000 00:00

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

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

      Совсем без задержек код крутить я че-то "очкую". С прерываниями обмен всегда нормально проходит?


  1. strvv
    00.00.0000 00:00
    +1

    моё небольшое замечание:
    1. если время такта принять 0.5с, т.е. считать такты по 0.5 секунд, то половинки секунд уйдут и будут целые числа, описывающие текущие состояния.
    не 0, 3, (4, 8.5, 9.5, 10.5), (8, 9, 10), 11,
    а 0, 6, (8, 17, 19, 21), (16, 18, 20), 22.
    2. и если в структуре иметь максимальное количество тактов (N), от 0..N-1, то номер можно брать по модулю, и смещение будет автоматически учитываться.

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


    1. OldFashionedEngineer Автор
      00.00.0000 00:00

      Я пока в эти тонкости не заходил. Чтобы не было путаницы, в примерах время считается в единицах миллисекунд. Все числа получаются большие, но целые.


      1. aumi13
        00.00.0000 00:00

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


  1. alexhott
    00.00.0000 00:00

    Этой статьи могло бы и не быть если бы создатели ардуины не придумали delay и не показали на нем пример мигания светодиодом.

    В первом своем проекте на мк, пытался использовать Arduino IDE, и их библиотеки, но стукнувшись о занятый таймер при попытке напрямую инициировать ШИМ как мне надо, ушел от ардуино.


    1. OldFashionedEngineer Автор
      00.00.0000 00:00
      +1

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


  1. Parondzhanov
    00.00.0000 00:00

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

    Можно данный алгоритм нарисовать на языке ДРАКОН, например, так:

    Здесь тоже есть ваш бесконечный цикл, на него указывают черные треугольники.

    На Хабре по языку ДРАКОН см. посты:


    1. OldFashionedEngineer Автор
      00.00.0000 00:00

      Я сторонник "натуральных ощущений", если вы понимаете, о чем я))) Я с ассемблера ещё начинал программировать. Хотя, я считаю визуальные языки более перспективными.