Введение
На просторах форума мне попалась интересная задача на тему внешнего устройства расширяющего возможности световой сигнализации автомобиля, процесс решения которой показался мне очень показательно-поучительным в плане демонстрации процесса анализа исходных данных и, собственно разработки программного обеспечения микроконтроллера (ПО МК) на основе этого анализа.
По моим наблюдениям даже начинающие разработчики не испытывают больших проблем с разработкой схемотехники для своих разработок, а вот с разработкой программ для микроконтроллеров (встроенных вычислительных машин) часто возникают трудности, казалось бы, на ровном месте.
Я надеюсь что эта статья, как раз, будет интересна как наглядный пример формирования программы микроконтроллера на языке С из некоторого произвольного описания назначения устройства, описания его функциональности, схемотехники, внешних воздействий-сигналов... Надеюсь, что интересна будет не только начинающим разработчикам встроенного ПО, но и тем кто считает себя опытным программистом.
Постановка задачи
Изначально задача была сформулирована в виде достаточно развернутого вопроса написать некоторый код в этой теме, потом было вот это уточнение задачи где упоминаются в том числе:
Функциональные возможности устройства, устройство работает следующим образом:
• При коротком нажатии на рычаг поворотов – устройство выдаст серию запрограммированных импульсов мигания лампами поворотов;
• При длительном нажатии на рычаг поворотов (больше одного моргания) – эффекта удлинения не будет;
• При переключении из одного направления в момент удлинения поворотов в другое, повороты прекращают мигать по первому направлению и начинают мигать по второму.
В конце концов была приведена схема: Схема устройства подключенного к управляемой системе.
Анализ схемотехники
На исходной схеме даже не выделено устройство, работу которого надо запрограммировать, поэтому я приведу здесь дополненную схему рис.1 на которой схемотехника, которая относится к нашему разрабатываемому устройству выделена в рамке.
Я уверен, что автор задачи, совершенно справедливо, считал эту информацию очевидной, поэтому не делал соответствующих пометок на схеме. Но, дело в том, что при разработке ПО надо четко определить что мы имеем дело с новым устройством, которое подключено и некоторым образом воздействует на существующую систему световой сигнализации автомобиля (далее везде просто систему).
Например, когда мы рассуждаем о состояниях, мы должны четко различать состояния указанной системы и состояния нашего устройства, это очень важно! Про состояния я напишу подробнее позже.
Рис.1
Также я заменил изображения транзисторов на изображением абстрактного ключа и подписал ключи (ключ - элемент электрической цепи, выключатель), потому что тут важно понимать, что устройство добавляет соответствующие ключи (Доп. Ключ значит дополнительный) параллельно уже существующим в системе, и через них получает возможность управлять работой системы. И нам не важно, с точки зрения программирования/управления как реализованы эти управляемые ключи (транзисторы это, микросхемы, реле ...)
Также подписаны входы (inL, inR, inC) через которые МК получает информацию о состоянии системы (о состоянии поворотников и реле прерывателя, которые в свою очередь отображают состояние или положение подрулевого рычага) и выходы управления ключами outR, outL.
Первая версия алгоритма
Теперь надо попробовать последовательно записать действия контроллера в ответ на некоторое изменение состояния системы, в нашем случае оно определяется состоянием подрулевого рычага.
Я предпочитаю это делать С-подобной записью или псевдо кодом.
Вот что у меня получилось с первого раза:
startPoint: While(!(RLC = getRLC()) & 0b101)){}; cntrBitsSet(oldRLC& 1, (oldRLC >> 2) & 1);//замкнуть паралельные ключи //в соответствии со значениями R and L! timerStart(); oldRLC = RLC; RLC = getRLC(); While(RLC == oldRLC || RLC == 0) { RLC = getRLC();};//включился поворотник – ждем и считаем время Period = timerStop(); If(period > ShortTime) goto longTimeBranch; ShortTimeBranch: timerStart(3 seconds); While(timerIsActive()) {}; Goto startPoint; longTimeBranch: cntrBitsClearAll();//РАзОмкнуть паралельные ключи Goto startPoint;
Поясню подробно:
- Начнем с нейтрального состояния рычага, очевидно мы должны ждать изменения на входах inL, inR, вот так например:
- While(!(RLC = getRLC()) & 0b101)){};
- операция & позволяет игнорировать значения в позициях заданных нулем в константе (0b101 – игнорируем значение во второй позиции)
- и здесь мы крутимся в пустом цикле пока не появится единица хотя бы в одном бите соответствующем линии inL или inR.
- каждый раз запоминаем код, который состовляется из значений на линиях inL, inC, inR в переменной RLC
- Раз мы вышли из цикла значит рычаг изменил состояние, допустим мы влючили правый поворотник, получим код 0b011: inL=0 не включен, inC=1 сначала включен при переключении-еще не прерывался, inR = 1 включен в соответствии с допущением. Поскольку это первая версия (НЕ идеальная) здесь я делаю предположение о поведении нашего устройства в этой ситуации. Итак я решил что оно должно продублировать положение ключей которые были открыты в системе на своих ключах:
- Для этого надо просто выставить полученные значения для inL, inR которые соответствуют состоянию ключей системы на ключи устройства (тут, кстати, можно заметить что мы пропустили начальную инициализацию состояния ключей нашего устройства-!-пометим для исправления в следующей версии!), на outR, outL вот так:
- cntrBitsSet(RLC& 1, (RLC >> 2) & 1);//замкнуть паралельные ключи, в коде выше ошибка, значения храняться в переменной RLC, а не oldRLC
- И это была ошибка – теперь мы не увидим когда рычаг вернется в исходное состояние, потому что ключ устройства дублирует действие ключа системы, но что бы эту ошибку увидеть надо рассмотреть дальнейшее поведение устройства (я то не первый раз этот алгоритм прохожу, вместо МК!)
- Здесь (я опять предпологаю, хотя предположение основано на успешном опыте реализации подобных алгоритмов, что) мы должны сравнивать текущее состояние входных линий с тем сохраненным значением, которое «выпустило» нас из предыдущего цикла и посчитать время до следующего изменения, ведь мы должны различать короткие и длинные нажатия! Поэтому:
- timerStart();//запускаем счет времени, который будет остановлен в функции timerStop() дальше
- oldRLC = RLC;//запоминаем «выпустившее» нас значение в новой переменной
- RLC = getRLC();//получаем код значений на линиях inL, inC, inR и сохраняем в переменной RLC
- While(RLC == oldRLC || RLC == 0)//крутимся в цикле пока не изменится значение на линиях inL, inR и (RLC == 0) игнорируем прерывания от регулятора, потому что inC=0 гасит все линии в ноль!
- { RLC = getRLC();};//постоянно получаем код значений на линиях inL, inC, inR и сохраняем в переменной RLC
- Раз мы вышли и из этого цикла значит рычаг опять изменил состояние, и значит мы дождались когда рычаг изменил свое положение, допустим мы вернули его в исходное положение. Когда я писал эту версию я просмотрел, что условие в предыдущем цикле «не увидит» такое изменение, потому что (см.2.3) воздействие рычага продублировано нашим устройством! Но что бы закончить описание алгоритма мы пока будем исходить из этого ошибочного предположения, что мы все таки выйдем из цикла при возврате рычага в исходное положение. И если мы вышли из цикла мы делаем следующее:
- Period = timerStop();//останавливаем счет времени, и получаем отсчитанный период в переменую, это время на которое включали поворотник!
- If(period > ShortTime) goto longTimeBranch;//здесь мы можем решить какое это нажатие было, длинное или короткое и перейти в терминах языка С на соответствующюю ветку
- Рассмотрим действия в случае короткого нажатия. мы должны оставить в покое наши дополнительные ключи, которые уже включены (п.2.) в соответствии с нужным положением рычага и просто отсчитать время на которое мы хотим «удлиннить» это короткое нажатие (например 3 секунды)
- ShortTimeBranch://метка для ветки короткого нажатия
- timerStart(3);//запускаем отсчет времени
- While(timerIsActive()) {};//ждем когда время выйдет
- Goto startPoint;//возвращаемся в исходное состояние, если за время отсчета периода нажатия произоидет какое то нажатие его обработка будет отложена и, соответственно, получим ошибку в определении времени этого нажатия, этот случай тоже подлежит отдельному рассмотрению -!- тоже пометим для исправления в следующей версии!
- Рассмотрим действия в случае длинного нажатия, которое соответственно не надо удлиннять, но поскольку мы продублировали открытие ключей системы дополнительными ключами системы в п.2. Мы соответственно должны вернуть их в исходное состояние, примерно в таких операциях:
- longTimeBranch: //метка для ветки длинного нажатия
- cntrBitsClearAll();//РАзОмкнуть дополнительные ключи
- Goto startPoint; //возвращаемся в исходное состояние, если за время отсчета периода нажатия произойдет какое то нажатие его обработка будет отложена и, соответственно, получим ошибку в определении времени этого нажатия, этот случай тоже подлежит отдельному рассмотрению -!- уже пометили (п.5.4) для исправления в следующей версии!
Как видим алгоритм наш замкнулся, вернулся в начало. Так же можно убедиться, что он не зависит от того, в какое состояние переключается рычаг из нейтрального-начального положения, остается исправить отмеченные замечания на следующих итерациях, но это уже тема следующей статьи где собственно и должен быть реализован идеальный алгоритм.
Я, честно говоря, хотел коснуться проблемы реализации счета не только времени, а количества периодов горения/гашения поворотников тоже, но материала уже и так с избытком, надо посмотреть будет ли интересна хоть кому то эта довольно специфичная тема.
Заключение
Как уже упоминалось это только первая версия алгоритма, и она конечно НЕ идеальная.
Умозрительное «выполнение» придуманного алгоритма разработчиком вместо МК позволяет нам сформулировать и подготовится к рассмотрению частных ситуаций, для которых эту версию алгоритма надо изменить и/или дополнить.
Уже сейчас можно видеть, что разработка алгоритма это процесс умозрительного моделирования действий процессора и реакции на него управляемой системы, то есть мы формулируем содержание операций, которые должен выполнять контроллер для контроля за состоянием «вверенного ему оборудования» и для управления этим оборудованием в соответствии с этим состоянием. А реализация кода этих операций эта тема для еще одной статьи и это составляющая подведет нас уже к идеальной программе.
Надеюсь данная статья позволит осознать специфику и сложность реализации ПО для казалось бы примитивных устройств, таких как мы рассмотрели.
Комментарии (10) | Я собрал (0) | Подписаться
Для добавления Вашей сборки необходима регистрация
[Автор]
Вообще, принцип будет работать и при наличии дребезга.
[Автор]
[Автор]
Там вся элементарная логика уже элементарно реализована, что конструировать то?
Может проще было рассмотреть систему как конечный автомат, а потом рассказать как входа выходы обрабатывать? (Дребезг контактов, изменение состояния входных данных в момент изменения состояния системы (система не конечный автомат с дискретными состояниями, а с переходными процессами, так как есть таймеры) и так далее?
[Автор]
Надеюсь станет понятнее.
[Автор]