Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Мы привыкли к линейным алгоритмам. Нас учили строить их на информатике в школах, затем на младших курсах в колледжах и институтах. Это был бейсик или паскаль в консоли. Часто учителя просто выдавали программу, указанную в учебном плане, а для чего такое программирование может понадобиться на практике, особо никто не рассказывал. Да что там говорить, большинство повседневных задач мы сами формулируем в виде линейных алгоритмов.
Но так ли хорош этот метод для программирования микроконтроллеров, и есть ли какая-то простая и доступная альтернатива линейным алгоритмам? Я предлагаю вместе разобраться в этом вопросе.
❯ Предисловие
Не ищите в этой статье конечных автоматов, их тут точно нет. Но зато есть необходимые предпосылки, которые позволят в дальнейшем лучше понять, что к чему. Статья состоит из трех частей. Первая содержит философию, на основе которой будут строиться практические примеры второй и третьей части. Практические задачи я постарался подобрать несложные, чтобы не отвлекаться на смежные области и полностью погрузиться в программирование.
Если вы только начинаете постигать программирование Arduino, этот материал именно для вас. А опытные программисты могут оставить свои рекомендации в комментариях, или сразу пройти к следующим не менее интересным статьям на хабре.
Эта статья предполагает продолжение. Я постараюсь с ним сильно не затягивать, но все будет зависеть от реакции читателя.
Текст статьи содержит основные идеи решения поставленных задач. Листинги и пояснения к ним ищите под спойлерами. Я спрятал их там, чтобы вы не терялись в объемном тексте. Желаю всем приятного и полезного чтения.
❯ Хочешь программировать микроконтроллер, думай как микроконтроллер
Линейные программы пришли из такой математической дисциплины, как линейное программирование. И, простите за тавтологию, задумывалось это все для решения систем линейных уравнений. То есть программа представляет собой последовательность вычислительных операций и пересылки данных между ячейками памяти. Для построения графиков функций можно было рассчитать одно и тоже уравнение в некотором диапазоне значений с помощью циклов. А ветвления подходили для задач, подобных поиску дискриминанта.
Так было в эпоху дорогого машинного времени, когда компьютеры, по сути, выполняли функцию дорогущих навороченных калькуляторов. На них проводили расчеты для нужд науки или планирования в области народного хозяйства. Но со временем машинное время стало значительно дешевле, и микропроцессорную технику стали использовать для все более широкого и приземленного круга задач, к примеру для управления твоей кофеваркой.
Только представьте себе алгоритм управления кофеваркой в виде многочлена. Скорее всего это возможно, но потребует не дюжих математических способностей. А мы ищем путь по проще.
Микроконтроллеру редко приходится решать задачи типа: «У Маши было два яблока, а у Вани одно...», хотя некоторые задачи бывают и не на много сложнее. Встречаются алгоритмы управления, которые можно выразить математической функцией. Это различного рода регуляторы, к примеру ПИД-регулятор. Но и они обычно являются составной частью какого-то алгоритма управления.
Пока компьютеры занимали целые машинные залы, для выполнения функций управления различными объектами строили специализированные электронные схемы. Естественно, что вопросу синтеза этих управляющих схем посвящали специальные теории и разрабатывали специальные методы. Именно тут и стоит вспомнить про теорию автоматического управления, в частности карты Карно, конъюнктивные и дизъюнктивные нормальные формы. Вот где все это было полезно.
Вот так выглядел блок управления двигателями в магнитофоне Электроника 003 в 80-х годах
Не удивительно, что многие программисты в конечном итоге пришли к тому, чтобы использовать некоторые идеи уже готовой теории автоматического управления для программирования этих самых управляющих автоматов.
Но это было почти век назад, управляющие автоматы не имели сложных графических интерфейсов и от них не требовалось работать в глобальной сети. Наверное, по этой причине так сложно бывает выразить алгоритм управления через чистый автомат Мура.
Вот такие предпосылки были для появления новых методик в программировании управляющих систем на основе микроконтроллеров. И тут пока не сформировалась единая точка зрения, ведь работы ведутся сравнительно недавно. Всего-то пару десятков лет назад для микроконтроллеров мейнстримом был ассемблер. А сегодня даже на яве умудряются прошивки писать.
Так какие же недостатки имеют линейные алгоритмы для микроконтроллеров? В следующем разделе попробуем решить несколько задач в виде привычного линейного алгоритма и проанализируем результаты.
❯ Линейный алгоритм, все «за» и «против»
В качестве первой задачи возьмем светофор. Его часто используют для примера, но мне это не очень нравится, т.к. есть сложности с однозначным выделением входных сигналов. А вот любят эту задачу за то, что в ней просто выделить конечные состояния.
Пускай работа светофора определяется графиком переключений, показанным на рисунке.
Все эксперименты уже традиционно буду проводить в Proteus на плате Arduino Uno. Для визуализации работы устройства воспользуюсь виртуальной моделью светофора. На физическом макете можно использовать светодиоды с подходящими по номиналам резисторами.
Думаю, что составить алгоритм в привычном его представлении ни для кого не составит труда. Мой вариант вы можете увидеть на рисунке.
Текст программы напишу в 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);
}
Давайте попробуем проанализировать полученный результат. Хочу отметить коварство кажущейся простоты такого программирования. С одной стороны построение линейных алгоритмов хорошо подходит под образ мышления неподготовленного для решения задач программирования человека. Но с другой стороны такой подход требует высокой степени концентрации сразу ко всему объему программного кода.
Лично я дважды допустил ошибку, пока писал этот код: перепутал константы при передачи входных параметров функциям, вместо интервала времени вписал номер вывода контроллера; в конце забыл выключить желтый светодиод, в результате чего во втором цикле красный сигнал сразу сопровождался желтым.
Обе эти ошибки конечно же вызваны моим небрежным отношением к программе. Но нельзя списывать со счетов и тот факт, что код получился весьма неоднородным по своей структуре. Контролировать подобный код становится непропорционально труднее с увеличением его объема.
Также хочу заметить, что при выполнении программы, написанной по данному алгоритму, наш микроконтроллер основное машинное время будет тратить на формирование интервалов времени между переключениями сигналов светофора. Это сильно ограничивает функциональные возможности нашей программы. Добавить еще какую-то параллельную задачу в наш код будет очень затруднительно.
Чтобы убедиться в истинности моих утверждений, предлагаю вам самостоятельно модифицировать программу для управления светофором на перекрестке по графику на рисунке ниже. Для этого дополним схему еще одной моделью светофора.
Сколько попыток вам понадобится, чтобы программа заработала без ошибок? Естественно, ошибки будут. Но они будут связаны не с тем, что алгоритм имеет какую-то невероятную сложность. И конечно же, ни с тем, что вы не знаете синтаксис. Ошибки будут обусловлены именно тем, что программный код сложно контролировать.
С вашего позволения я не буду рисовать алгоритм, а сразу приведу код программы. Алгоритм не будет принципиально отличаться от предыдущего ничем кроме дополнительного нагромождения блоков.
Алгоритмический обработчик светофора для перекрестка
//--------------------------------------------------
//Порты для управления светофором 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. Алгоритм может функционировать только дискретно. То есть после каждого завершения работы алгоритма, его необходимо вызывать повторно.
Рассмотрим возможную схему обработки светофора в виде однопроходного алгоритма.
Для удобства понимания того, как это все будет работать, можно представить будущую программу в виде электрической схемы, а точнее в виде ее функциональной диаграммы. Как-будто мы будем проектировать светофор на цифровых микросхемах.
Задающий генератор формирует базовые интервалы времени, на основе которых должен формироваться график переключения сигналов светофора. Для этого счетчик ведет подсчет количества тактовых сигналов и передает накопленное значение логике формирования выходных сигналов.
Блок логики формирования выходных сигналов также синхронизируется от задающего генератора, чтобы гарантировать точность моментов переключения сигналов светофора относительно счетчика.
Каждый функциональный блок, входящий в состав представленной схемы, имеет свое время отклика на входное воздействие, определяемый временем срабатывания блока. Очевидно, что это время не должно превышать период базового интервала времени, иначе нормальная логика работы системы управления может быть нарушена.
При этом длительность базового интервала должна быть подобрана так, чтобы не превышать допустимую погрешность установки сигналов светофора на графике. В идеальном случае базовый интервал должен иметь значение, кратное для каждого момента переключения сигналов светофора. Тогда погрешность формирования выходных сигналов будет обусловлена только смещением на время отклика блоков, и не превысит значение базового интервала.
И как бы мы не старались, все равно в итоге приходим к линейному алгоритму. Хотя, несомненно, представление программы в виде функциональной диаграммы здорово облегчает дальнейшее программирование.
Полный текст программы я разместил под спойлером.
Однопроходный обработчик светофора
//--------------------------------------------------
//Порты для управления светофором
#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мс.
Функциональная диаграмма светофора на перекрестке может выглядеть следующим образом.
Обратите внимание, что управление вторым светофором я вывел в отдельный функциональный блок. Это возможно потому, что первый функциональный блок не блокирует выполнение программы, а заканчивает свою работу в каждом повторении цикла. Следовательно, второй блок мы можем также выполнить в виде однопроходного кода, и разместить его следом за уже готовым блоком первого светофора. К тому же это дает еще один неоспоримый плюс: мы можем использовать уже готовую часть программу, не нужно будет тратить дополнительное время для ее написания и проверки.
Замечу, что оба блока управления первым и вторым светофором взаимодействуют с одним и тем же счетчиком и источником тактового сигнала. Это возможно потому, что на графике переключений оба светофора связанны друг с другом единой логикой работы.
Для удобства составления алгоритма программы, конструкцию множественного выбора, которая управляет светофором, мы можем рассматривать как некий "черный ящик" (точнее как "белый ящик", т.к. его содержимое нам известно, но термин "черный ящик" звучит загадочнее).
Тогда мы можем разместить в нашей программе два аналогичных "черных ящика", каждый из которых будет управлять своим светофором.
Алгоритм работы второго "черного ящика" будет совершенно аналогичен первому, это становится очевидным, если продлить сигналы светофоров на графике, как показано на рисунке ниже. Также на рисунке видно, что оба светофора имеют одинаковые интервалы времени. Различие в работе первого и второго светофоров сводится к начальной фазе (показано как φ на графике).
Выходит, что оба "черных ящика" совершенно идентичны, их различия сводятся только к номерам портов, которые управляют светофорами. Но значение счетчика для второго "черного ящика" следует передавать со смещением.
Если модифицировать предыдущий код «в лоб», получается очень массивно. Я разместил этот вариант для тех, кто еще не очень владеет синтаксисом.
Однопроходный обработчик светофора для перекрестка. Вариант 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);
}
}
Если задача не очень критична с точки зрения аппаратных ресурсов, то я предпочитаю оптимизировать код в угоду читаемости. Такой подход минимизирует вероятность возникновения логических ошибок и упрощает дальнейшую поддержку кода.
Так как алгоритм работы обоих светофоров полностью идентичен, вынесем его в отдельную функция. А характеристики светофоров будем передавать из отдельных структур при вызове этих функций.
Однопроходный обработчик светофора для перекрестка. Вариант 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);
}
}
Если вы еще не разобрались как следует со структурами в языке С, под спойлером для вас я оставил разбор этой программы.
Язык С относится к неструктурированным языкам. Для примера сравните его с Паскалем, где четко определен порядок и назначение блоков кода. Эта особенность позволяет программисту формировать свою структуру программы, что делает код более выразительным.
Моя программа начинается с блока макроопределений. Они фактически формируют интерфейс программиста, который позволяет быстро менять основные параметры программы. Команды разбиты на блоки, которые определяют имена портов для управления сигналами светофора и интервалы времени переключения сигналов светофора. Интервалы времени будут общие для обоих светофоров.
Имя "TICK" для базового интервала времени выбрано как дань традиции. Такое имя часто использовалось в различных операционных системах для определения времени, выделяемого для обработки задач.
Для хранения параметров светофора в программе объявлена структура "TrafficLight". Для удобства программирования и получения более коротких записей эта структура переопределена как тип данных "TrafficLight_t". Благодаря этому в дальнейшем будет меньше мороки при передаче экземпляров структуры в качестве входных параметров функций.
В конструкции: "typedef struct TrafficLight{...} TrafficLight_t;" — имя структуры "TrafficLight" можно было бы опустить. Объявление выглядело бы следующим образом: "typedef struct {...} TrafficLight_t;". Эта запись сообщает компилятора о намерениях программиста размещать в памяти какие-то данные в формате, который указан между фигурными скобками. Выделение памяти при этом не происходит.
Далее в программе определяются экземпляры структуры "TrafficLight_t". Имена "trafficLight1" и "trafficLight2" могут рассматриваться как переменные, т.е. для них уже будет выделено место в памяти микроконтроллера.
Стоит обратить внимание на форму инициализации полей структуры, которую я использовал. При помощи точки (оператор обращения к полю структуры) я явно указываю имена полей, в которые записываются данные. Эта форма более наглядна и снижает вероятность перепутать, какие данные для какого поля предназначены. Но, к сожалению, Arduino IDE не поддерживает эту фишку целиком. Порядок инициализаторов должен совпадать с тем, как они определены при объявлении шаблона структуры. Не допускается пропускать поля при инициализации. Спасибо и на этом, т.к. некоторые компиляторы для микроконтроллеров и такого не поддерживают.
Далее в программе производится определение функции "trafficLight_action()". Мы заявляем компилятору о своих намерениях использовать это имя как функцию, и определяем формат ее параметров. Само объявление функции будет в конце программы. Это позволяет расширить область имени функции и не загромождать код перед описанием основной логики программы.
Функция принимает два параметра. Первый — это указатель на структуру, которая хранит параметры светофора. Благодаря этому возможно обрабатывать два светофора с разными портами и интервалами времени с помощью одной и той же функции. Второй параметр — это значение времени относительно начала на графике работы светофора. Это позволит обрабатывать два светофора со смещением начальной фазы относительно друг друга.
Обратите внимание, как в функции "setup()" выполнена настройка портов микроконтроллера. Вместо прямого указания номера вывода на плате Arduino UNO, я передаю поле структуры "trafficLight1", которое содержит соответствующее значение. Это не очень хорошо влияет на размер генерируемого кода, но положительно сказывается на универсальности текста программы. Здесь более уместно было бы использовать макросы из начала программы. Но я написал так для демонстрации синтаксиса.
В начале функции "loop()" мы снова видим цикл "for", который выполняет счет времени работы светофора. Для обеспечения функционирования программного интерфейса, о котором мы говорили в начале описания листинга, предел счетчика ограничен записью "TIME_RED + TIME_GREEN + 6*TIME_PULS + TIME_YELLOW2", ее вычисление компилятор выполнит на стадии сборки программы, и это не повлияет на производительность. Можно было бы заранее определить подходящий макрос, который заменил бы такую длинную запись и сделал бы ее более осмысленной, но это значение используется в программе однократно, и я решил оставить так.
Хотя с точки зрения дальнейшего апгрейда функциональности, лучше бы сделать отдельную переменную, в которую суммировались бы поля структуры с соответствующими параметрами светофора. К примеру, можно было бы изменять интервалы времени через USART. Но, парой, сложно предусмотреть все сразу.
Замечу, что увеличение счетчика так, как это сделано в программе "counter += TICK", не самое оптимальное решение для микроконтроллера. Счетчик объявлен как «uint16_t», и занимает 2 байта в памяти микроконтроллера. Оперативная память у нашей платы 8-ми битная. Обработка счетчика будет занимать значительно больше тактов, чем если бы он был 8-ми битным. Время на графике можно было бы считать в количестве тактов, а не в мили секундах, тогда бы и 8-ми битной переменной хватило. Но читаемость кода получилась бы менее наглядной. Задача, которую мы решаем, занимает далеко не весь вычислительный потенциал Arduino UNO, поэтому я и не стал заморачиваться.
Посмотрим, как выполнена обработка светофоров. Вызов функции "trafficLight_action()" производится два раза подряд. Но в каждом вызове она получает параметры разных светофоров. С помощью оператора получения адреса "&" мы передаем ссылку на структуру. При этом в стек локальных переменных функции попадает только два байта адреса. Если бы мы передавали не ссылку на структуру, а структуру целиком, то при каждом вызове функции в стек переписывались бы все ее поля. Это не лучшим образом отразилось бы на производительности программы, да и сожрало бы лишнюю оперативку.
Обращение к структуре через указатель таит за собой некоторую опасность. Поля структуры могут быть безвозвратно модифицированы внутри функции. В некоторых случаях это будет наоборот полезно. А в некоторых недопустимо.
При обработке второго светофора значение счетчика смещается на время, пока на первом светофоре горит красный: "counter + TIME_RED — TIME_YELLOW1". Результат такого сложения может вывести счетчик за пределы графика переключений светофора. Защита от таких ситуаций должна быть предусмотрена в самой функции "trafficLight_action()". Это хороший прием особенно для тех случаев, когда значение какого-то параметра программа получает из вне, и вы заранее не можете быть уверены, что значение будет введено в корректном диапазоне.
Обращаю внимание, что символ "%" — это не получение процентов, а получение остатка от целочисленного деления (5%2 == 1, два помещается в пятерку целиком два раза и единичка остается в остатке). А запись "%=" это сокращение с операцией присвоения результата (а %= b эквивалентно a = a % b).
Остальная часть функции обработки светофора последовательно проверяет значение счетчика. Если он совпадает с метками на графике, производится переключение сигналов.
Зачем все эти «пироги», если можно было написать класс? Все банально просто. Далеко не все компиляторы для микроконтроллеров поддерживают классы, поэтому я привык обходиться без них.
Моя программа начинается с блока макроопределений. Они фактически формируют интерфейс программиста, который позволяет быстро менять основные параметры программы. Команды разбиты на блоки, которые определяют имена портов для управления сигналами светофора и интервалы времени переключения сигналов светофора. Интервалы времени будут общие для обоих светофоров.
//--------------------------------------------------
//Порты для управления светофором 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. Процесс разработки и отладки программы более трудоемкий и требует адаптации мышления;
В данном примере мы рассмотрели возможность параллельной обработки нескольких задач. Хотя задачи были достаточно линейными. Комбинация сигналов светофора всегда сменяется в одном и том же порядке опираясь на значение времени. А как быть, если последовательность смены состояний устройства управления может зависеть от внешних факторов? Об этом поговорим в следующем разделе.
❯ Использование флагов для управления однопроходной программой
Использование флагов в программировании имеет глубокие ассемблерные корни. Такие абстракции, как Arduino или HAL-драйвер под STM32, для работы с периферией микроконтроллера используют готовые библиотеки с удобными интерфейсами. В ассемблерных программах для этих же целей приходилось напрямую манипулировать отдельными разрядами управляющих регистров в памяти микроконтроллера. Биты в этих регистрах ввода/вывода по сути являются флагами состояния периферийных устройств.
К примеру, при обмене по прерываниям, процессор «узнает» о том, что какое-то периферийное устройства готово к обмену данными, по специальному флагу — биту в регистре флагов прерываний. А для разрешения прерываний необходимо устанавливать соответствующий флаг в регистре масок прерываний.
Регистр флагов чем-то похож на мишень для биатлона. Событие — это попадание в мишень, которое отмечается поднятием флажка.
Программные флаги использовались для хранения состояния программы по аналогии с аппаратными флагами. Это была вынужденная мера, связанная с необходимостью экономить оперативную память, которой у микроконтроллеров было не так много. В одном регистре памяти можно было разместить несколько программных флагов по одному биту на каждый. Работа с ними выполняется с помощью логических операций.
Современные микроконтроллеры, такие как STM32, имеют колоссальные объемы оперативной и программной памяти. Это позволяет применять для их программирования объектно-ориентированную парадигму. Но компактные 8-ми битные устройства все равно еще имеют широкое применение. Хотя уровень оптимизации современных Си компиляторов позволяет обойтись без ассемблера и для них, сами подходы к программированию заставляют использовать ассемблерные приемы.
Получается, что программный флаг — это один бит в регистре, или отдельная переменная, принимающая одно из двух состояний. Флаг может быть связан с каким-либо событием или состоянием программы. Обычно используется для обмена между отдельными фрагментами кода в отдельных функциях, или телах условных операторов. Для этого часто обладают глобальной областью видимости.
Давайте воспользуемся флагами для того, чтобы реализовать вызывной светофор, график работы которого показан на рисунке. Обратите внимание, что переключения сигналов также, как и в прошлых задачах, имеют фиксированную последовательность. Периодичность переключения сигналов светофора также имеет фиксированные значения. Но время работы зеленого сигнала светофора теперь определяется кнопкой.
Для выполнения этой задачи, добавим в схему кнопку. Про особенности подключения кнопки к Arduino я писал в этой статье.
Если проанализировать график, на нем можно выделить два события, которые влияют на работу светофора. Счет времени работы светофора ведется до тех пор, пока не произойдет его переключение с красного на зеленый сигнал. Второе событие — это нажатие на кнопку, после которого счет времени возобновляется. С этой точки зрения можно сказать, что кнопка взаимодействует не со светофором, а со счетчиком времени. Чтобы управлять состоянием счетчика в программе введем специальный флаг. Сброс флага должен произойти в тот момент, когда выключается красный и включается зеленый сигнал светофора, установка — при нажатии кнопки.
Для удобства дальнейшего программирования составим функциональную диаграмму разрабатываемого устройства управления вызывным светофором.
В схему добавлен блок, работающий аналогично 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. С чем едят конечный автомат
Перейдя по ссылке, вы можете посмотреть, как я реализовал полнофункциональный светофор без применения микроконтроллеров, только на жесткой логике.