СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

H Реализация многоуровневого меню для Arduino с дисплеем в черновиках Tutorial



Здравствуйте хабравчане! Мне всегда были интересны всякие устройства, и еще больше была интересна возможность создавать их самому. И вот, однажды после очередной мысли блуждающей в голове была приобретена Arduino Mega 2560 и начались эксперименты. Как и большинство из тех кто становился обладателем Arduino, я, вдоволь помигав светодиодами, покрутив шаговые двигатели, решил двигаться дальше и создать что-нибудь более полезное. Когда идея о будущем устройстве сформировалась в голове я приступил к разработке. Успешная реализация идеи подразумевает решение нескольких комплексных задач. Одной из таких задач является создание удобного интерфейса для настройки.

Устройство имеет довольно большое количество параметров (несколько десятков) и в дальнейшем их количество скорей всего будет увеличиваться. Управлять таким количеством параметров отдельными кнопками и потенциометрами, как и контролировать точные значения параметров — невозможно, поэтому устройство оснащено дисплеем и клавиатурой, а управление параметрами осуществляется через меню. Для удобства настройки меню многоуровневое. Значения параметров имеют различный тип, это может быть как вкл/выкл, числовое значение, время и т. д. Так как у параметра может быть большое количество значений то имеется также возможность задания параметра путем удержания кнопки. Теперь подробней как это реализуется.

Клавиатура


Обеспечение работы клавиатуры немного более сложная задача чем кажется на первый взгляд человеку никогда не имеющему дело с электроникой. Так как мы имеем меню то для управления нам понадобятся следующие кнопки:
«Вверх» (передвижение вверх по меню, выбор следующего значения для параметра)
«Вниз» (передвижение вниз по меню, выбор предыдущего значения для параметра)
«Вправо» (вход в подменю, начало редактирования параметра)
«Влево» (выход из подменю, выход из редактирования параметра)
дополнительно нам пригодится еще несколько кнопок, например:
«Старт/Стоп»
«Быстрое действие 1»
«Быстрое действие 2»
«Быстрое действие 3»

«Быстрое действие 100»

Я намеренно указал большое количество кнопок, так как устройство будет развиваться и, с большой вероятностью, по мере использования, настройка некоторых параметров будет вынесена на главную панель. Также на панель будут выноситься кнопки для выполнения быстрых действий. Из этого следует что использовать для каждой кнопки отдельный вход микроконтроллера будет неправильно так как нам может либо не хватить входов, либо мы будем тратить много времени на обработку этих входов. Но существует очень распространенное решение этой задачи, при этом понадобится лишь один аналоговый вход. Суть решения заключается в следующем:
Клавиатура собирается из множества последовательно соединенных резисторов которые представляют из себя делитель напряжения. Один конец цепи подключается к земле, другой конец цепи подключается к +5 В. Далее все кнопки одним контактом подключаются к входу микроконтроллера а другим контактом к местам соединения резисторов. Схема такой клавиатуры выглядит следующим образом:


Таким образом в момент нажатия кнопки на вход микроконтроллера подается определенное напряжение которое зависит от нажатой кнопки. Микроконтроллер измеряя это напряжение с помощью АЦП понимает какая кнопка нажата.

Дребезг контактов


Следующая задача заключается в решении проблемы дребезга контактов. Используя выключатели для света, различные кнопки, клавиатуры и т. д. мы часто не задумываемся о том, что происходит внутри этой кнопки. Складывается ошибочное впечатление что кнопка имеет два фиксированных состояния — включено/выключено. На самом же деле когда мы нажимаем на кнопку то замыкание цепи происходит не сразу. В течении короткого промежутка времени(короткого только для человека, но не для микроконтроллера осуществляющего миллионы операций в секунду) контакты вибрируют, искрят, шумят и т. д. и кнопка за этот промежуток времени может имитировать большое количество срабатываний (нажатий и отжатий). Таким образом если эта кнопка используется, например, для счетчика нажатий то без защиты от дребезга контактов при однократном нажатии наш микроконтроллер может подумать что кнопку нажали 3 раза, или 5, или 20 раз и т. д. Все зависит от качества кнопки, частоты опроса кнопки, от погоды на Марсе и т. д. В большинстве случаев проблема решается считыванием значения кнопки не сразу после нажатия а через некоторое время.

Программно защиту от дребезга контактов реализуют несколькими способами, и, к сожалению довольно часто, для этого используют функцию Delay(). Я бы не рекомендовал ее использовать так как в момент ее выполнения микроконтроллер гоняет балду вместо того чтобы заниматься делами которыми он должен заниматься, таким образом мы теряем часть производительности. Вместо использования Delay() я запоминаю время нажатия кнопки и сам факт того что было нажатие. В рабочем цикле loop проверяется было ли нажатие и если нажатие было то сравнивается время нажатия с текущим временем. Если разница выше определенного значения (определенной задержки за которое дребезг успевает устаканиться) то производится считывание значения нажатой кнопки.

Такая реализация заодно позволяет реализовать функционал «зажатой кнопки» т. е. если нам необходимо увеличить значение параметра, например, с 1 до 100, то нам не придется 100 раз нажимать кнопку. Мы просто нажмем и будет удерживать ее. В момент нажатия значение однократно изменится с 1 на 2 и через несколько секунд удержания кнопки значение начнет изменяться с большой скоростью. Запоминание времени позволяет также легко задать несколько зависимостей, например если мы держим кнопку 1 секунду то значение начинает изменяться каждые 500 миллисекунд. Если мы держим кнопку 5 секунд то значение начинает изменяться каждые 25 миллисекунд и т. д.

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

пример кода который это реализует:
//пин к которому подключена панель управления
int KeyButton1Pin=0;
//значение с панели управления
int KeyButton1Value=0;
//последнее время когда на панели не было нажатых кнопок
long KeyButton1TimePress=0;
//задержка перед считыванием состояния панели управления после нажатия
long KeyButton1Latency=100000;
//метка означающая что нажатие кнопки на панели было обработано
int KeyButton1WasChecked=0;
//время после которого удерживание кнопки начинает засчитываться как многократные быстрые нажатия
long KeyButton1RepeatLatency=1500000;
//вспомогательная переменная для обработки повторных нажатий
long KeyButton1RepeatTimePress=0;
//переменная для хранения времени между временем когда не было зажатых кнопок и временем проверки
long KeyButton1TimeFromPress=0;
//Переменные для обработки времени для обработки событий клавиатуры
long KeyBoardTime1=0;
long KeyBoardTime2=0;
long KeyBoardTimeInterval=25000;
void setup()
{
pinMode(KeyButton1Pin, INPUT);
}

void UpPress()
{
//действия при нажатии кнопки "вверх"
}
void DownPress()
{
//действия при нажатии кнопки "вниз"
}

void ButtonPress()
{
  if ((KeyButton1Value>125) and (KeyButton1Value<135))
    {
      UpPress(); 
    }
  if ((KeyButton1Value>255) and (KeyButton1Value<262))
    {
       DownPress();
    }
}

void KeyBoardCalculate()
{
  //Часть отработки нажатия клавиши
  KeyButton1Value=analogRead(KeyButton1Pin); 
  //если сигнал с кнопки нулевой то обнуляем метку обработки нажатия
  if ((KeyButton1Value<=50) or (KeyButton1Value>=1000))
    {
      //Сохраняем время последнего сигнала без нажатой кнопки
      KeyButton1TimePress=micros(); 
      KeyButton1WasChecked=0;
      KeyButton1RepeatTimePress=0;
    } 
        
  KeyButton1TimeFromPress=micros()-KeyButton1TimePress;
  //исключаем шумы
  if ((KeyButton1Value>50) and (KeyButton1Value<1000))
    {
      //отработка первого нажатия
      if ( ((KeyButton1TimeFromPress)>KeyButton1Latency) and (KeyButton1WasChecked==0))
        {
           KeyButton1Value=analogRead(KeyButton1Pin);  
           ButtonPress();
           KeyButton1WasChecked=1;
           KeyButton1RepeatTimePress=0;
        }
      
      //отработка повторных нажатий  
      if ( ((KeyButton1TimeFromPress)>(KeyButton1RepeatLatency+KeyButton1RepeatTimePress)) and (KeyButton1WasChecked==1))
        {
           KeyButton1Value=analogRead(KeyButton1Pin);  
           ButtonPress();
           KeyButton1RepeatTimePress=KeyButton1RepeatTimePress+500000;
        }
    }
  
}


void loop() {
  //проверка таймера для обработки нажатий клавиатуры
  KeyBoardTime2=micros();
  if ((KeyBoardTime2-KeyBoardTime1)>KeyBoardTimeInterval) 
    {
      KeyBoardTime1=KeyBoardTime2;
      KeyBoardCalculate();
    }
}




Работает следующим образом:
В цикле loop постоянно запоминаем текущее время в переменную KeyBoardTime2 и сравниваем ее с переменной KeyBoardTime1. В случае если разница между данными переменными будет больше чем переменная KeyBoardTimeInterval то мы сохраняем в переменную KeyBoardTime1 переменную KeyBoardTime2 и запускаем процедуру для обработки нажатий KeyBoardCalculate(). Дальнейшая обработка нажатий проверяется в процедуре KeyBoardCalculate(). Такой механизм позволяет нам сократить количество проверок нажатия кнопки, и более менее фиксировать частоту проверок без использования Delay.

В процедуре KeyBoardCalculate() первым делом считывается уровень напряжения с делителя напряжения клавиатуры
KeyButton1Value=analogRead(KeyButton1Pin);

Функция analogRead в случае использования Arduino Mega 2560 кодирует аналоговое значения напряжения входа в цифровое 10-битное значение, т. е. если на входе мы будем иметь 5 В то значение будет равно 1023, если на входе 0 В то значение равно 0, если на входе 2.5 В то значение равно 512 и т. д. Таким образом в переменной KeyButton1Value хранится напряжение получаемое с клавиатуры. В моей клавиатуре в качестве рабочего диапазона я принял диапазон от 50 до 1000, т. е. в случае если на входе напряжение из этого диапазона то микроконтроллер думает что нажата одна из кнопок. Если значение не попадает в этот диапазон то микроконтроллер думает что нет ни одной нажатой кнопки.
После того как считано значение с клавиатуры программа определяет нажата ли хоть одна кнопка
if ((KeyButton1Value<=50) or (KeyButton1Value>=1000))

Если ни одна кнопка не нажата это условие выполнится и программа сохранит последнее время когда не была нажата ни одна кнопка в переменную KeyButton1TimePress, после чего обнулит переменную KeyButton1WasChecked, говорящую о том что есть необработанные нажатия и переменную KeyButton1RepeatTimePress, использующуюся при обработке повторных нажатий.
Далее в переменную KeyButton1TimeFromPress заносится разница между текущим временем и последним временем когда не была нажата ни одна кнопка.

Затем выполняется проверка нажатия кнопки:
 if ((KeyButton1Value>50) and (KeyButton1Value<1000))

Если кнопка нажата то данное условие выполняется и мы проверяем факт первого нажатия кнопки
if ( ((KeyButton1TimeFromPress)>KeyButton1Latency) and (KeyButton1WasChecked==0))

Если время которое прошло с момента последнего нажатия кнопки больше чем время в течении которого устаканивается дребезг контактов (переменная KeyButton1Latency) и это нажатие мы еще не обработали (KeyButton1WasChecked==0) то запоминаем значение клавиатуры:
 KeyButton1Value=analogRead(KeyButton1Pin);

запускаем процедуру ButtonPress() (которая определяет какое действие надо сделать для этой кнопки), запоминаем что обработали первое нажатие (KeyButton1WasChecked=1) и обнуляем переменную для обработки повторных нажатий (KeyButton1RepeatTimePress=0).

Если время которое прошло с момента последнего нажатия кнопки больше чем время после которого мы считаем что нажатие повторное+время следующей обработки повторного нажатия ((KeyButton1TimeFromPress)>(KeyButton1RepeatLatency+KeyButton1RepeatTimePress)) и первая обработка нажатия уже произошла (KeyButton1WasChecked==1) то сохраняем значение клавиатуры:
KeyButton1Value=analogRead(KeyButton1Pin);

запускаем процедуру ButtonPress() (которая определяет какое действие надо сделать для этой кнопки), увеличиваем время через которое будет снова обработано удерживание кнопки
KeyButton1RepeatTimePress=KeyButton1RepeatTimePress+500000;


Процедура выполнения соответствующего для кнопки действия выглядит так:
void ButtonPress()
{
  if ((KeyButton1Value>125) and (KeyButton1Value<135))
    {
      UpPress(); 
    }
  if ((KeyButton1Value>255) and (KeyButton1Value<262))
    {
       DownPress();
    }
}

т.е. проверяется какое напряжение выдает клавиатура и выполняется соответствующее действие, например, если измеренное напряжение имеет значение от 125 до 135 то нажата кнопка Вверх, если от 255 до 262 то кнопка Вниз и т.д. Количество кнопок и процедур их обрабатывающих ограничивается практически только вашей фантазией, уровнем шумов вашей клавиатуры и битностью АЦП.

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

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

Меню


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



Изначально мы имеем адрес элемента 1. Зная его адрес мы можем перейти на сам элемент 1, получить его значение и узнать адрес следующего элемента. Таким образом мы можем последовательно проходить по списку, узнавать значения и адреса следующих элементов. Беда такого списка в том что он однонаправленный. Т. е. перейдя в элемент 5 мы не можем вернуться в элемент 4. Для этого нам придется пройти весь список заново. Эта проблема решена в двунаправленных списках:

Двунаправленные списки:



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

Для наглядности мы можем представить этот список в виде трех массивов, где массив Value[] хранит значения элементов. Массив Parent[] хранит индексы родителей либо 0 если родителей нет, и массив Child[] хранит индексы дочерних элементов и 0 если дочерних элементов нет. Таким образом двунаправленный список который был описан выше будет выглядеть так:
N Value Parent Child
1 1 0 2
2 2 1 3
3 3 2 4
4 4 3 5
5 5 4 0

Если мы например хотим сделать этот список кольцом то он будет выглядеть так:
N Value Parent Child
1 1 5 2
2 2 1 3
3 3 2 4
4 4 3 5
5 5 4 1

С этим думаю все понятно.

Давайте теперь представим что наша структура выглядит не как цепочка элементов а как дерево элементов. В таком случае элемент по прежнему может иметь одного родителя, но количество дочерних элементов теперь может быть любым. Такую структуру наши три массива уже описать не способны так как они хранят только два адреса. Эту проблему можно решить следующим образом — введем дополнительное условие. Пусть все дочерние элементы одного родительского элемента будут стоять строго по порядку. В дополнение к этому я изменю массив Child[] на массив ChildFirst[] и добавлю еще один массив ChildEnd[]. Пусть эти два массива хранят в себе индексы первого и последнего дочернего элемента соответственно и ноль если у элемента нет дочерних элементов.

Таким образом, например, если у нас есть один родительский элемент и три дочерних то структура будет выглядеть следующим образом:
N Value Parent ChildFirst ChildEnd
1 1 2 2 5
2 2 0 3 0
3 3 0 4 0
4 4 0 5 0

Или если изобразить графически то примерно так:



Теперь у нас есть возможность описывать деревья. Давайте также введем некий элемент номер ноль. Пусть он будет родителем всех элементов для которых значение Parent равно нулю. А теперь взглянем на небольшой пример:

Используя тот же принцип можно описать и другую структуру, например:
N Value Parent ChildFirst ChildEnd
0 Main Menu 0 1 3
1 Menu1 0 4 7
2 Menu2 0 10 14
3 Menu3 0 8 9
4 Param1_1 1 0 0
5 Param1_2 1 0 0
6 Param1_3 1 0 0
7 Param1_4 1 0 0
8 Param3_1 3 0 0
9 Param3_2 3 0 0
10 Param2_1 2 0 0
11 Param2_2 2 0 0
12 Param2_3 2 0 0
13 Param2_4 2 0 0
14 Param2_5 2 0 0

Графически это будет выглядеть примерно так:



Если мы нарисуем структуру которую описывают эти данные то получим основное меню Main Menu, состоящее из трех пунктов — Menu1,Menu2,Menu3. Каждый из этих пунктов включает в себя нескольких параметров ParamX_Y. Собственно это все. Мы получили меню с некоторым количеством параметров. Для возможности изменять параметры я добавил еще несколько массивов:

MenuTypeCode[]-массив содержит цифру которая показывает тип пункта меню. Например, если это пункт меню, а не параметр то значение равно 0, если это редактируемый параметр то для целого числа это будет цифра 1, для времени это цифра 2, для значений On|Off это цифра 3 и т. д. сколько душе угодно.

MenuValue[]-массив содержит значение конкретного параметра который мы настраиваем.

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

N Value Parent ChildFirst ChildEnd MenuTypeCode MenuValue
0 Main Menu 0 1 3 0 0
1 Menu1 0 4 7 0 0
2 Menu2 0 10 14 0 0
3 Menu3 0 8 9 0 0
4 Param1_1 1 0 0 1 50
5 Param1_2 1 0 0 2 3600
6 Param1_3 1 0 0 3 0
7 Param1_4 1 0 0 1 120
8 Param3_1 3 0 0 2 7200
9 Param3_2 3 0 0 3 1
10 Param2_1 2 0 0 1 8
11 Param2_2 2 0 0 2 0
12 Param2_3 2 0 0 3 1
13 Param2_4 2 0 0 1 5
14 Param2_5 2 0 0 2 60

В итоге получилось меню MainMenu где например в пункте Menu1 есть параметр Param1_2 имеющий значение MenuValue=3600. Так как мы знаем что значение MenuTypeCode для этого элемента равно 2 то мы трактуем это значение как время, т.е. например поделим на 3600 секунд и получим 1 час. Для элемента Param1_3 значение MenuTypeCode равно 3 и мы будем трактовать MenuValue=0 не как ноль, а как Off. Забегая вперед скажу что в итоге это будет выглядеть так:



Осталось лишь реализовать программно правильный вывод на дисплей и правильное редактирование параметров. Перед описанием кода еще пару слов о железе. В качестве дисплея я использовал дисплей JN12864J с контроллером Sitronix ST7920 (разрешение дисплея 128х64 как раз подходит для отображения меню). Для работы с дисплеем использовал библиотеку U8glib. Клавиатура подключена к входу A0.

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

Ранее описано что после обработки нажатия кнопки запускается процедура ButtonPress(), которая в свою очередь определяет какая кнопка была нажата и в зависимости от нажатой кнопки запускает соответствующие процедуры:
Для кнопки «Вверх» — процедура UpPress(), используется для передвижения по меню вверх, в режиме редактирования увеличивает значение параметра.
Для кнопки «Вниз» — процедура DownPress(), используется для передвижения по меню вниз, в режиме редактирования уменьшает значение параметра.
Для кнопки «Вправо» — процедура RightPress(), используется для входа в меню либо для входа в режим редактирования параметра.
Для кнопки «Влево» — процедура LeftPress(), используется для выхода из дочернего меню либо для выхода из режима редактирования.

В самом начале работы программы мы формируем меню запуском процедуры MenuSetup() В этой процедуре полностью описывается вся структура меню. Также задается начальное положение в меню:
MenuNowPos=1;

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

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

Таким образом если мы нажали кнопку Вверх или кнопку Вниз то производится проверка включен или нет режим редактирования. Если режим редактирования включен то проверяем тип элемента с индексом MenuNowPos и в соответствии с его типом меняем его значение. Для кнопки Вниз например:

    //если находимся в режиме редактирования 
  if (MenuEdit==1)
    {
      //проверяем какого типа меню и проверяем соответствующие ограничения, также контроллируем в зависимости от значения приращение
      //или уменьшение значения
      if (MenuTypeCode[MenuNowPos]==1) 
        {
          if (MenuValue[MenuNowPos]>0)
            {
            MenuValue[MenuNowPos]=MenuValue[MenuNowPos]-1;
            }
        }
      //Если тип временной интервал  
      if (MenuTypeCode[MenuNowPos]==2) 
        {
          if (MenuValue[MenuNowPos]>0)
            {
            MenuValue[MenuNowPos]=MenuValue[MenuNowPos]-1;
            }
        }
      //Если пункт меню бинарный то инвертируем значение
      if (MenuTypeCode[MenuNowPos]==3) 
        {
          MenuValue[MenuNowPos]=(MenuValue[MenuNowPos]+1) % 2;  
        }
    }  


Если режим редактирования выключен то проверяем есть ли соседний дочерний элемент у выделенного, и, если такой элемент есть то передвигаемся на него вверх или вниз, в зависимости от того какая кнопка нажата. Например для кнопки Вверх:
  //если не находимся в режиме редактирования то кнопка используется для передвижения по меню
  if (MenuEdit==0)
  {
    //если текущая позиция в меню больше чем позиция первого элемента в этом меню то можно осуществлять передвижение.
    if (MenuChildFirst[MenuParent[MenuNowPos]]<MenuNowPos)
      {
        //осуществляем передвижение по меню на 1 пункт
        MenuNowPos=MenuNowPos-1;
        //при движении вверх проверяем где расположен выделенный пункт меню на дисплее
        //если выделенный пункт не упирается в край дисплея то также смещаем его на дисплее на 1 позицию
        if (MenuDrawPos>0)
          {
          MenuDrawPos=MenuDrawPos-1;  
          }
      }
  }


Аналогичная операция производится при нажатии кнопок Влево и Вправо. Если нажимаем кнопку Влево то проверяем находимся ли в режиме редактирования, если да выходим из него:
    if (MenuEdit==1)
      {  
      MenuEdit=0;
      }


есди нет то проверяем есть ли для текущего элемента родительский, и, если есть — переходим на него:
  //если не находимся в режиме редактирования
    if (MenuEdit==0)
      { 
        //если пункт меню содержит ненулевой индекс родителя (т.е. мы находимся внутри другого меню)
        if (MenuParent[MenuNowPos]>0)
          { 
            //то переходим на индекс родительского пункта меню
            MenuNowPos=MenuParent[MenuNowPos];
            //установка позиции на экарне, если количество пунктов меньше чем влезает на экране то выделенный пункт будет в самом низу но не в конце
            //иначе будет в самом конце
            if (MenuChildEnd[MenuParent[MenuNowPos]]-MenuChildFirst[MenuParent[MenuNowPos]]<MenuDrawCount)
              {
                MenuDrawPos=MenuNowPos-MenuChildFirst[MenuParent[MenuNowPos]];
              }
              else 
                {
                  MenuDrawPos=MenuDrawCount-1;
                }
          }
      }


Для кнопки Вправо алгоритм практически такой же. Проверяем есть ли дочерний элемент для текущего, и, если есть — переходим на него, если же нет и мы стоим на параметре то включаем режим редактирования:
void RightPress()
{
  //если код типа элемента отличается от нуля (т.е. выделенный элемент является параметром) то включаем режим редактирования
  if (MenuTypeCode[MenuNowPos]>0)
    {
    MenuEdit=1;  
    }
  //если код типа элемента равен нулю значит в данный момент выделен пункт меню и мы можем войти в него  
  if (MenuTypeCode[MenuNowPos]==0)
    {
    //обнуляем позицию выделенного пункта на экране
    MenuDrawPos=0;  
    //переходим на первый дочерний элемент для текущего элемента
    MenuNowPos=MenuChildFirst[MenuNowPos];
    }  
}

Остается лишь вывести все на экран. Для этого используется процедуры Draw() и DrawMenu()
Процедура Draw() имеет стандартный вид для библиотеки U8glib. Запускает обновление экрана и завершает свое выполнение когда обновление завершено. Ее вызов происходит в цикле loop с некоторой периодичностью.
void Draw()
{
  u8g.firstPage();   
  do 
    { 
      //прорисовка статуса калибровки
      DrawMenu();
    } while( u8g.nextPage() );
}


Процедура DrawMenu() отвечает непосредственно за вывод меню. Принцип прост — у нас есть несколько переменных:
MenuNowPos — индекс текущего выделенного элемента
MenuDrawPos — номер строки на экране в которой находится текущий выделенный элемент MenuNowPos
MenuDrawCount — максимальное количество отображаемых строк на экране.

Таким образом зная индекс выделенного элемента MenuNowPos мы можем определить его родительский элемент -MenuParent[MenuNowPos] а зная его родительский элемент мы можем определить все соседние элементы входящие в то же меню что и элемент MenuNowPos. Это элементы начина с MenuChildFirst[MenuParent[MenuNowPos]] и кончая MenuChildEnd[MenuParent[MenuNowPos]].
Таким образом не составит труда в любой момент времени вывести на экран либо все эти элементы если их мало, либо часть из них если их больше чем количество отображаемых на экране строк.
Так же не составит труда в процессе отображения этих элементов определить являются ли эти элементы параметрами:
if (MenuTypeCode[MenuNowPos]>0)

и, если являются то рядом с ними отобразить их текущее значение в зависимости от их типа, например, для бинарного типа:
        if (MenuTypeCode[MenuNowPos]==3) 
          { 
            if (MenuValue[MenuNowPos]==0) {u8g.print("Off");}
              else {u8g.print("On");}


Немного видео демонстрирующего работу меню. В видео есть и меню не помещающееся на экран, и разные типы параметров (цифры, время, On|Off) и многоуровневое меню (меню в меню в основном меню), и повторное срабатывание кнопки через некоторое время:



Полный код рабочей программы привожу ниже:
Полный код программы
#include "U8glib.h"
//указание пинов для использования дисплея, не обязательно брать пины аппаратного SPI
U8GLIB_ST7920_128X64_1X u8g(36, 38, 39);

//переменные для работы с клавиатурой
//пин к которому подключена панель управления
int KeyButton1Pin=0;
//значение с панели управления
int KeyButton1Value=0;
//последнее время когда на панели не было нажатых кнопок
long KeyButton1TimePress=0;
//задержка перед считыванием состояния панели управления после нажатия
long KeyButton1Latency=100000;
//метка означающая что нажатие кнопки на панели было обработано
int KeyButton1WasChecked=0;
//время после которого удерживание кнопки начинает засчитываться как многократные быстрые нажатия
long KeyButton1RepeatLatency=1500000;
//вспомогательная переменная для обработки повторных нажатий, после каждого повторного срабатывания переменная увеличивается
//сдвигая тем самым време следующего повторного срабатывания зажатой кнопки
long KeyButton1RepeatTimePress=0;
//переменная для хранения времени между временем когда не было зажатых кнопок и временем проверки
long KeyButton1TimeFromPress=0;
//Переменные используются для периодической обработки нажатий клавиатуры
//время прошлой обработки клавиатуры
long KeyBoardTime1=0;
//текущее время
long KeyBoardTime2=0;
//период обработки клавиатуры
long KeyBoardTimeInterval=25000;

//Переменные отвечающие за меню
//Массив с названиями меню
char* MenuNames[50];
//Тип элемента меню
//0-родительское меню
//1-целое число
//2-временной интервал (h:m:s, целое число но отображается как время)
//3-Вкл/Выкл (целое число, но отображается как On/Off)
//4-Расстояние (cm, целое число, но отображается как 0.хх метров)
int MenuTypeCode[50];
//Значение элемента меню
int MenuValue[50];
//Текущая позиция в меню, вложенность не важна т.к. меню представляет из себя список с ссылками на родительские 
//и дочерние пункты
int MenuNowPos=0;
//Режим редактирования (0-нет, просто выделен определенный параметр или пункт меню, 1-редактируем значение параметра)
int MenuEdit=0;
//номер элемента меню который является для данного родительским
//0-нет родительского элемента
int MenuParent[50];
//номер элемента меню который является для данного дочерним
//0-нет дочернего элемента
int MenuChild[50];
//Номер элемента дочернего меню который является первым
int MenuChildFirst[50];
//номер элемента дочернего меню который является последним
int MenuChildEnd[50];
//позиция меню в которой находится выделенный пункт на экране (например на экране отображается 3 пункта при этом выделен второй)
int MenuDrawPos=0;
//максимальное количество отображаемых на экране пунктов
int MenuDrawCount=4;


//переменные для таймера перерисовки
//время последней перерисовки
long DrawTime1=0;
//текущее время
long DrawTime2=0;
//интервал для перерисовки экрана
long DrawTimeInterval=200000;


void MenuSetup()
{
//Настройка меню
//задаем начальное положение в меню
MenuNowPos=1;
//Массив с названиями меню, индексами родительских элементов меню, начальными и конечными индексами дочерних элементов меню
//также задаем начальные значения параметров элементов и их тип
MenuNames[0]="Main Menu";
MenuTypeCode[0]=0;
MenuValue[0]=0;
MenuParent[0]=0;
MenuChildFirst[0]=1;
MenuChildEnd[0]=3;

MenuNames[1]="Menu1";
MenuTypeCode[1]=0;
MenuValue[1]=0;
MenuParent[1]=0;
MenuChildFirst[1]=4;
MenuChildEnd[1]=9;

MenuNames[2]="Menu2";
MenuTypeCode[2]=0;
MenuValue[2]=0;
MenuParent[2]=0;
MenuChildFirst[2]=10;
MenuChildEnd[2]=11;

MenuNames[3]="Menu3";
MenuTypeCode[3]=0;
MenuValue[3]=0;
MenuParent[3]=0;
MenuChildFirst[3]=12;
MenuChildEnd[3]=15;

MenuNames[4]="Param1_1";
MenuTypeCode[4]=1;
MenuValue[4]=0;
MenuParent[4]=1;
MenuChildFirst[4]=0;
MenuChildEnd[4]=0;

MenuNames[5]="Param1_2";
MenuTypeCode[5]=2;
MenuValue[5]=0;
MenuParent[5]=1;
MenuChildFirst[5]=0;
MenuChildEnd[5]=0;

MenuNames[6]="Param1_3";
MenuTypeCode[6]=3;
MenuValue[6]=0;
MenuParent[6]=1;
MenuChildFirst[6]=0;
MenuChildEnd[6]=0;

MenuNames[7]="Param1_4";
MenuTypeCode[7]=1;
MenuValue[7]=0;
MenuParent[7]=1;
MenuChildFirst[7]=0;
MenuChildEnd[7]=0;

MenuNames[8]="Param1_5";
MenuTypeCode[8]=2;
MenuValue[8]=0;
MenuParent[8]=1;
MenuChildFirst[8]=0;
MenuChildEnd[8]=0;

MenuNames[9]="Param1_6";
MenuTypeCode[9]=3;
MenuValue[9]=0;
MenuParent[9]=1;
MenuChildFirst[9]=0;
MenuChildEnd[9]=0;

MenuNames[10]="Param2_1";
MenuTypeCode[10]=3;
MenuValue[10]=0;
MenuParent[10]=2;
MenuChildFirst[10]=0;
MenuChildEnd[10]=0;

MenuNames[11]="Param2_2";
MenuTypeCode[11]=3;
MenuValue[11]=0;
MenuParent[11]=2;
MenuChildFirst[11]=0;
MenuChildEnd[11]=0;

MenuNames[12]="Param3_1";
MenuTypeCode[12]=1;
MenuValue[12]=0;
MenuParent[12]=3;
MenuChildFirst[12]=0;
MenuChildEnd[12]=0;

MenuNames[13]="Param3_2";
MenuTypeCode[13]=3;
MenuValue[13]=0;
MenuParent[13]=3;
MenuChildFirst[13]=0;
MenuChildEnd[13]=0;

MenuNames[14]="Param3_3";
MenuTypeCode[14]=3;
MenuValue[14]=0;
MenuParent[14]=3;
MenuChildFirst[14]=0;
MenuChildEnd[14]=0;

MenuNames[15]="Menu3_4";
MenuTypeCode[15]=0;
MenuValue[15]=0;
MenuParent[15]=3;
MenuChildFirst[15]=16;
MenuChildEnd[15]=17;

MenuNames[16]="Param3_4_1";
MenuTypeCode[16]=2;
MenuValue[16]=0;
MenuParent[16]=15;
MenuChildFirst[16]=0;
MenuChildEnd[16]=0;

MenuNames[17]="Param3_4_2";
MenuTypeCode[17]=3;
MenuValue[17]=0;
MenuParent[17]=15;
MenuChildFirst[17]=0;
MenuChildEnd[17]=0;
}


void setup()
{
  //настройка порта для клавиатуры
  pinMode(KeyButton1Pin, INPUT);
  //настройка дисплея
  u8g.setHardwareBackup(u8g_backup_avr_spi);
  if ( u8g.getMode() == U8G_MODE_R3G3B2 ) {
    u8g.setColorIndex(255);     
  }
  else if ( u8g.getMode() == U8G_MODE_GRAY2BIT ) {
    u8g.setColorIndex(3);       
  }
  else if ( u8g.getMode() == U8G_MODE_BW ) {
    u8g.setColorIndex(1);       
  }
  else if ( u8g.getMode() == U8G_MODE_HICOLOR ) {
    u8g.setHiColorByRGB(255,255,255);
  }
//формирование меню
MenuSetup();
}


void DrawMenu()
{
//временные переменные для отображения временных параметров  
int DrawHours=0;
int DrawMinutes=0;
int DrawSeconds=0;
  
u8g.setFont(u8g_font_fixed_v0);  
//вывод названия родительского меню вверху экрана
u8g.setPrintPos(5, 9);
u8g.print(MenuNames[MenuParent[MenuNowPos-MenuDrawPos]]);
u8g.drawLine( 0, 10, 123,10);
//переменная для вывода пунктов меню на экран
int DrawI=0;
//цикл для вывода пунктов меню на экран
for(DrawI=0; DrawI<MenuDrawCount;DrawI++)  
  {
    u8g.setPrintPos(5, 21+10*DrawI);
    if ((MenuChildFirst[MenuParent[MenuNowPos]]<=(MenuNowPos-MenuDrawPos+DrawI)) and 
      (MenuChildEnd[MenuParent[MenuNowPos]]>=(MenuNowPos-MenuDrawPos+DrawI)))
      { 
        u8g.print(MenuNames[MenuNowPos-MenuDrawPos+DrawI]);  
     
     
        u8g.setPrintPos(80, 21+10*DrawI);
        //Если целое число
        if (MenuTypeCode[MenuNowPos-MenuDrawPos+DrawI]==1) 
          {
            u8g.print(MenuValue[MenuNowPos-MenuDrawPos+DrawI]);  
          }
        
        //Если тип временной интервал  
        if (MenuTypeCode[MenuNowPos-MenuDrawPos+DrawI]==2) 
          {
            DrawHours=MenuValue[MenuNowPos-MenuDrawPos+DrawI] / 3600;
            DrawMinutes=(MenuValue[MenuNowPos-MenuDrawPos+DrawI] % 3600) / 60;
            DrawSeconds=(MenuValue[MenuNowPos-MenuDrawPos+DrawI] % 3600) % 60;
            u8g.print((String)DrawHours+":"+(String)DrawMinutes+":"+(String)DrawSeconds);
          }  
        //Если пункт меню бинарный
        if (MenuTypeCode[MenuNowPos-MenuDrawPos+DrawI]==3) 
          { 
            if (MenuValue[MenuNowPos-MenuDrawPos+DrawI]==0) {u8g.print("Off");}
              else {u8g.print("On");}  
          }     
      }
  }
  
  //если параметр сейчас не редактируется то отображение рамки вокруг выделенного пункта меню
  if (MenuEdit==0)
    {
    u8g.drawLine( 3, 12+10*MenuDrawPos, 70, 12+10*MenuDrawPos);
    u8g.drawLine(70, 12+10*MenuDrawPos, 70,22+10*MenuDrawPos);
    u8g.drawLine( 3,22+10*MenuDrawPos, 70,22+10*MenuDrawPos);
    u8g.drawLine( 3,  22+10*MenuDrawPos, 3,12+10*MenuDrawPos);
    }  
    
  //если параметр сейчас редактируется то отображение рамки вокруг значения параметра
  if (MenuEdit==1)
    {
      u8g.drawLine( 75, 12+10*MenuDrawPos, 122, 12+10*MenuDrawPos);
      u8g.drawLine(122, 12+10*MenuDrawPos, 122,22+10*MenuDrawPos);
      u8g.drawLine( 75,22+10*MenuDrawPos, 122,22+10*MenuDrawPos);
      u8g.drawLine( 75,  22+10*MenuDrawPos, 75,12+10*MenuDrawPos);
    }  
}


void Draw()
{
  u8g.firstPage();   
  do 
    { 
      //прорисовка статуса калибровки
      DrawMenu();
    } while( u8g.nextPage() );
}


//при завершении редактирования пункта меню происходит обновление настроек
void UpdateSettings()
{
  //здесь происходит обновление настроек
  //допустим мы имеем программу которая мигает лампочкой с частотой speed
  //допустим этот параметр speed задается в элементе с индексом 4 тогда нам надо написать такой код:
  /*if (MenuNowPos==4) {
      Speed=MenuValue[MenuNowPos]
      }
  */
}


//Процедура для обработки нажатия кнопки "вверх"
void UpPress()
{
  //если не находимся в режиме редактирования то кнопка используется для передвижения по меню
  if (MenuEdit==0)
  {
    //если текущая позиция в меню больше чем позиция первого элемента в этом меню то можно осуществлять передвижение.
    if (MenuChildFirst[MenuParent[MenuNowPos]]<MenuNowPos)
      {
        //осуществляем передвижение по меню на 1 пункт
        MenuNowPos=MenuNowPos-1;
        //при движении вверх проверяем где расположен выделенный пункт меню на дисплее
        //если выделенный пункт не упирается в край дисплея то также смещаем его на дисплее на 1 позицию
        if (MenuDrawPos>0)
          {
          MenuDrawPos=MenuDrawPos-1;  
          }
      }
  }

  //Если находимся в режиме редактирования
  if (MenuEdit==1)
    {
      //проверяем какого типа меню и проверяем соответствующие ограничения, также контроллируем в зависимости от значения приращение
      //или уменьшение значения
      //Если тип целое число то максимального ограничения нет (добавить потом чтоб бралось максимальное значение из меню)
      if (MenuTypeCode[MenuNowPos]==1) 
        {
          MenuValue[MenuNowPos]=MenuValue[MenuNowPos]+1;
        }
      //Если тип временной интервал  
      if (MenuTypeCode[MenuNowPos]==2) 
        {
          MenuValue[MenuNowPos]=MenuValue[MenuNowPos]+1;
        }
      //Если пункт меню бинарный то инвертируем значение
      if (MenuTypeCode[MenuNowPos]==3) 
        {
          MenuValue[MenuNowPos]=(MenuValue[MenuNowPos]+1) % 2;  
        }
    }
}


//Процедура для обработки нажатия кнопки "вниз"
void DownPress()
{
  //если не находимся в режиме редактирования
  if (MenuEdit==0)
  {
    //проверяем не является ли текущий пункт последним дочерним элементом
    if (MenuChildEnd[MenuParent[MenuNowPos]]>MenuNowPos)
      {
        //если не является то двигаемся на 1 пункт вниз
        MenuNowPos=MenuNowPos+1;
        //проверяем упираемся ли мы в край экрана. максимальное число элементов меню на экране задано переменной MenuDrawCount
        if ((MenuDrawPos<MenuDrawCount-1) and (MenuDrawPos<MenuChildEnd[MenuParent[MenuNowPos]]-MenuChildFirst[MenuParent[MenuNowPos]]))
          {
          //если в край экрана не упираемся то также сдвигаем позицию на экране на 1 пункт вниз
          MenuDrawPos=MenuDrawPos+1;  
          }
      }
  }
    //если находимся в режиме редактирования 
  if (MenuEdit==1)
    {
      //проверяем какого типа меню и проверяем соответствующие ограничения, также контроллируем в зависимости от значения приращение
      //или уменьшение значения
      if (MenuTypeCode[MenuNowPos]==1) 
        {
          if (MenuValue[MenuNowPos]>0)
            {
            MenuValue[MenuNowPos]=MenuValue[MenuNowPos]-1;
            }
        }
      //Если тип временной интервал  
      if (MenuTypeCode[MenuNowPos]==2) 
        {
          if (MenuValue[MenuNowPos]>0)
            {
            MenuValue[MenuNowPos]=MenuValue[MenuNowPos]-1;
            }
        }
      //Если пункт меню бинарный то инвертируем значение
      if (MenuTypeCode[MenuNowPos]==3) 
        {
          MenuValue[MenuNowPos]=(MenuValue[MenuNowPos]+1) % 2;  
        }
    }  
}  
  
  
//Процедура для обработки нажатия кнопки "влево"  
void LeftPress()
{
  //если не находимся в режиме редактирования
    if (MenuEdit==0)
      { 
        //если пункт меню содержит ненулевой индекс родителя (т.е. мы находимся внутри другого меню)
        if (MenuParent[MenuNowPos]>0)
          { 
            //то переходим на индекс родительского пункта меню
            MenuNowPos=MenuParent[MenuNowPos];
            //установка позиции на экарне, если количество пунктов меньше чем влезает на экране то выделенный пункт будет в самом низу но не в конце
            //иначе будет в самом конце
            if (MenuChildEnd[MenuParent[MenuNowPos]]-MenuChildFirst[MenuParent[MenuNowPos]]<MenuDrawCount)
              {
                MenuDrawPos=MenuNowPos-MenuChildFirst[MenuParent[MenuNowPos]];
              }
              else 
                {
                  MenuDrawPos=MenuDrawCount-1;
                }
          }
      }
    //если находимся в режиме редактирования то выключаем режим редактирования  
    if (MenuEdit==1)
      {  
      MenuEdit=0;
      //запускаем процедуру для обновления настроек. это необходимо для того чтобы параметры меню которые мы поменяли начали действовать
      //в программе для которой мы используем это меню
      UpdateSettings()
      }
}
  
  
//Процедура для обработки нажатия кнопки "вправо"   
void RightPress()
{
  //если код типа элемента отличается от нуля (т.е. выделенный элемент является параметром) то включаем режим редактирования
  if (MenuTypeCode[MenuNowPos]>0)
    {
    MenuEdit=1;  
    }
  //если код типа элемента равен нулю значит в данный момент выделен пункт меню и мы можем войти в него  
  if (MenuTypeCode[MenuNowPos]==0)
    {
    //обнуляем позицию выделенного пункта на экране
    MenuDrawPos=0;  
    //переходим на первый дочерний элемент для текущего элемента
    MenuNowPos=MenuChildFirst[MenuNowPos];
    }  
}
  

void ButtonPress()
{
  //130
  if ((KeyButton1Value>125) and (KeyButton1Value<135))
    {
      UpPress(); 
    }
  //258  
  if ((KeyButton1Value>255) and (KeyButton1Value<262))
    {
      DownPress();
    }
  //65  
  if ((KeyButton1Value>60) and (KeyButton1Value<70))
    {
    LeftPress();  
    }
  //195  
  if ((KeyButton1Value>190) and (KeyButton1Value<200))
    {
    RightPress();
    }
}


void KeyBoardCalculate()
{
  //Часть отработки нажатия клавиши
  KeyButton1Value=analogRead(KeyButton1Pin); 
  //если сигнал с кнопки нулевой то обнуляем метку обработки нажатия
  if ((KeyButton1Value<=50) or (KeyButton1Value>=1000))
    {
      //Сохраняем время последнего сигнала без нажатой кнопки
      KeyButton1TimePress=micros(); 
      KeyButton1WasChecked=0;
      KeyButton1RepeatTimePress=0;
    } 
        
  KeyButton1TimeFromPress=micros()-KeyButton1TimePress;
  //исключаем шумы
  if ((KeyButton1Value>50) and (KeyButton1Value<1000))
    {
      //отработка первого нажатия
      if ( ((KeyButton1TimeFromPress)>KeyButton1Latency) and (KeyButton1WasChecked==0))
        {
           ButtonPress();
           KeyButton1WasChecked=1;
           KeyButton1RepeatTimePress=0;
        }
      //отработка повторных нажатий  
      if ( ((KeyButton1TimeFromPress)>(KeyButton1RepeatLatency+KeyButton1RepeatTimePress)) and (KeyButton1WasChecked==1))
        {
           ButtonPress();
           //различная скорость обработки повторений, если держим кнопку меньше 5 секунд то повторная обработка кнопки раз в 0.4с, 
           //если кнопка удерживается дольше 5 секунд то время следующего повторного срабатывания не увеличивается и происходит максимально быстро
           if (KeyButton1TimeFromPress<(KeyButton1RepeatLatency+5000000)) {
             KeyButton1RepeatTimePress=KeyButton1RepeatTimePress+400000;
             }
        }
    }
}


void loop()
{
  //проверка таймера для обработки графики, аналогично с обработкой клавиатуры 
  //обновление графики происходит не чаще заданного интервала DrawTimeInterval
  DrawTime2=micros();
  if ((DrawTime2-DrawTime1)>DrawTimeInterval) 
    {
      DrawTime1=DrawTime2;
      Draw();
    }
  //проверка таймера для обработки нажатий клавиатуры
  //обработка нажатий происходит не чаще заданного интервала KeyBoardTimeInterval
  KeyBoardTime2=micros();
  if ((KeyBoardTime2-KeyBoardTime1)>KeyBoardTimeInterval) 
    {
      KeyBoardTime1=KeyBoardTime2;
      KeyBoardCalculate();
    }
}



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

P.S.2: А чтобы настройки параметров не пропадали после перезагрузки устройства достаточно выполнить несколько
действий:
1. Подключить в начале программы библиотеку EEPROM.h строкой
#include <EEPROM.h>

2. В конец процедуры MenuSetup() добавить строки:
int i=0;
for (i=0;i<50;i++){
  MenuValue[i]=(EEPROM.read(i*2-1) << 8);
  MenuValue[i]=MenuValue[i]+(EEPROM.read(i*2-2));
  }

3. В процедуру UpdateSettings() добавить строки:
  EEPROM.write(MenuNowPos*2-2, lowByte(MenuValue[MenuNowPos]));
  EEPROM.write(MenuNowPos*2-1,highByte(MenuValue[MenuNowPos]));

+33
15331

комментарии (50)

+1
Meklon ,  
Спасибо. Очень поможет в разработке) я как раз специфическое железо для лабораторных нужд собираю. Примитивное, возможно, но для нашей лаборатории чрезвычайно важное. Просто я врач и базиса подобного у меня нет)
0
eta4ever ,  
При большом количестве параметров все равно нужна цифровая матричная клавиатура. В таком случае доступ, скажем, к параметру 5-11-33 не является адской болью.
0
ntfs1984 ,  
Жалко что после ресета все пропадет :(
+2
kumbr_87 ,   * (был изменён)
Добавил в конце статьи описание по сохранению параметров в EEPROM. После этого микроконтроллер можно без проблем перезагружать и выключать, при включении он будет считывать настройки параметров меню из EEPROM и соответственно при каждом изменении параметра обновлять его значение в EEPROM. Единственный нюанс, предварительно надо произвести хоть один раз запись всех параметров, иначе при включении будет считываться мусор.
Ну и значение нулевого элемента меню я не сохраняю так как считаю его неиспользуемым (это главное меню содержащее в себе лишь название главного меню и индекс первого и последнего элементов в главном меню)
0
Meklon ,  
Кстати, да. Мне сейчас параметры хардкодом зашивать приходится в разделе Опции прошивки. Но это неудобно.
0
Potok ,  
Настоятельно рекомендую перед тем, как что-то в EEPROM писать, закрыть прерывания, а также включить детектор brown-out. Не знаю, может, в скетчах ардуины уже это предусмотрели, но при работе с AVR-кой напрямую эти условия критичны, иначе высок риск получить мусор вместо данных. Да и сделать пару копий настроек было б не лишним, т.к. даже при соблюдении всех предосторожностей регулярно дуркует этот EEPROM. Много всего по этому поводу гугл знает, да и на хабре что-то всплывало где-то…
+1
jar_ohty ,  
Очень расточительным представляется занятие аналоговых входов кнопками. Тем более, если речь об Mega2560, у которой цифровых входов полно. Но могу подкинуть рационализацию: если резисторы у кнопок делать не так, как сделали вы, а по принципу матрицы R-2R, то можно будет нажимать одновременно несколько кнопок. Но количество их придется ограничить 4-5 штуками.
0
Meklon ,  
У Меги и аналоговых как грязи) зависит от проекта.
0
michaelkl ,   * (был изменён)
Функция analogRead в случае использования Arduino Mega 2560 кодирует аналоговое значения напряжения входа в цифровое 10-битное значение, т. е. если на входе мы будем иметь 5 В то значение будет равно 1023, если на входе 0 В то значение равно 0, если на входе 2.5 В то значение равно 512 и т. д.

А что будет, если потом вы замените источник питания, либо параметры источника изменятся со временем так, что выдаваемое им напряжение немного изменится? Скажем, вместо 5 В станет 4.9 В. Контроллеру это не помешает, а вот «коды» клавиш «уплывут». И чем больше клавиш вы подключаете на один вход, тем больше шансов, что коды выйдут из диапазонов.
Это я как программист, мало смыслящий в электронике, спрашиваю.
0
AxisPod ,  
Ну с делителем такое однозначно случится. Но если взять сначала напряжение, с одного из вводов, а затем уровень с того ввода использовать, то можно уровни для кнопок корректировать программно.
0
michaelkl ,  
Да, но тогда мы один ввод используем для замера напряжения, а второй для анализа нажатой клавиши. Итого, два входа АЦП расходуется, что накладно. Если я правильно понял вашу идею.
Хотя можно сэкономить вход АЦП за счёт одного цифрового выхода: при появлении единицы на этом выходе подавать на вход АЦП напряжение в обход резисторов делителя (алгоритм анализа нажатий клавиш при этом естественно отключаем), производить его замер и пересчитывать все уровни. Эту процедуру автокорекции можно запускать периодически.
Но неужели нельзя реализовать поддержку клавиатуры проще?
0
AxisPod ,  
Ну да, 2 входа. Тут решение в лоб, делать клавиатуру через делитель тоже решение в лоб. Но на 1м пине не думаю, что найдется лучше решение. С 2мя уже можно как-то жить.
0
UnknownType ,  
Видны следующие независимые улучшения работы с кнопками:
1) замкнуть SA4 навсегда и добавить S(N) и R(N+1) по вкусу. R1 убрать. В setup() опросить состояние KeyButton1Pin и соотнести его с ожидаемым значением. При необходимости отнормировать по ходу.
2) в случае если во время Reset нажата любая кнопка, запускать процедуру обучения клавиатуры и сохранять весовые коэффициенты в eeprom.
+1
Alexeyslav ,  
А с тремя цифровыми — вообще не парится, подключить I2C расширитель портов и зафигачить матричную клавиатуру 4x4 кнопки, а если не жалко диодов то в два раза больше.
0
kumbr_87 ,  
Все гораздо проще, напряжение ни куда не уплывает потому что клавиатура подключается к встроенному линейному стабилизатору Arduino. А на случай небольших изменений и шумов в программе как раз используется не конкретное значение уровня а диапазоны, например:
if ((KeyButton1Value>125) and (KeyButton1Value<135))
{
UpPress();
}
0
Meklon ,  
У меня этот стабилизатор выдаёт 5.14 вольт, что расстраивает. Думаю, как это исправить. Может добавить ещё что-то?
0
Alexeyslav ,  
не парится по этому поводу. У них ТКН не важный, отстроишь его ровно на 5 вольт, так на сквозняке снова уплывёт. Не нужно бороться с ветряными мельницами. Если нужно потенциметрическое измерение, привяжи опорный уровень АЦП к напряжению питания, он тогда выдаст в коде не реальное напряжение на входе а коэффициент деления потенциометра(или делителя напряжения из резисторов). Кстати, вход висящим в воздухе держать нельзя — у него сопротивление пару гигаом — будет реагировать на любую электростатику.
0
Meklon ,  
У меня стоит задача измерения реального сопротивления — написал ниже.
0
Alexeyslav ,  
Если есть образцовый резистор, то измерение сопротивления сводится к измерению соотношения делителя из резисторов, а решение этой проблемы читай выше…
0
Meklon ,  
Не получится. У меня диапазон 50-2 500 000 Ом.
0
kumbr_87 ,   * (был изменён)
Зависит еще от точности которая вам нужна. При таком диапазоне с точностью 50 Ом у вас будет 50 000 значений. Т.е. понадобится либо 16-ти битный АЦП. При 2.5МОМ ток будет очень мал и АЦП ардуины скорей всего не сможет правильно замерить напряжение. Есть вроде еще способы с использованием то ли мультиплексора то ли аттенюатора, для измерения малой кровью больших диапазонов. Но проще наверное будет прикупить АЦП с большей битностью и точный источник опорного напряжения. Есть еще проблема в измеряемом токе.
0
Alexeyslav ,  
Точный ИОН не нужен если измерять не сопротивление а соотношение сопротивлений, тогда вся точность измерений будет определяться только точностью образцового резистора. А если использовать ИОН, то точность измерения будет зависеть от 1) образцового резистора, 2) точности ИОН, 3) точности ИОН в МК. Три фактора против одного.
0
Meklon ,  
Это если делитель напряжений. А я время разрядки измеряю.
0
Alexeyslav ,  
Конденсатор может обладать нелинейностью, у вас это учтено?
0
Meklon ,  
Учтено, да.
0
Alexeyslav ,  
С какой точностью?
Линейность АЦП важна, или нужна конкретная цена деления?
С первым делом в нынешнее время довольно туго для АЦП высокой разрядности, а второе — не проблема.
Недавно видел платки с АЦП для тензорезисторов(весов) — 4 входа, кажется, АЦП с 24-битной точностью. Такая точность покроет необходимый диапазон измерения сопротивления в режиме делителя даже с одним образцовым резистором. Собственно в весах именно этот метод и используется и показания не зависят от напряжения питания.
Вам надо содрать типичную схему с весов, или даже использовать китайские весы с цифровым интерфейсом а вместо тензорезистора(моста тензорезисторов) подключить измеряемое сопротивление и образцовое(на 1МОм пойдет, наверно), только цифры масштабировать в свои величины.
0
Meklon ,  
Желательна точность финальная в рамках 1-2%. Если мерять в условных попугаях — микросекунды разрядки конденсатора, то точность уже 0.14%. Но, чтобы перевести ее в реальные Омы, нужно знать емкость точно (легко делается калибровкой на точном резисторе) и Vзарядки с Vопорным. Тогда можно посчитать согласно следующим формулам:

UC - Мгновенное значение напряжения конденсатора
U - Напряжение заряда общее
С - емкость кондесатора
R - сопротивление разряда.
UC =U/e^(t/RC),
U/UC=e^(t/RC)
R=t*ln(U/Uc)/C
C=t*ln(U/Uc)/R
0
kumbr_87 ,  
Можно не исправлять, по сути не важно какое напряжение он выдает, в разумных пределах конечно, главное чтобы это напряжение было стабильным, если он всегда выдает 5.14 то не составит труда откалибровать уровни для кнопок с небольшим запасом и проблем не будет. Можно также использовать стаб на 3.3В либо воспользоваться внешним, например lm7805. Стоит рублей 15 максимум наверное.

А так в предыдущем сообщении(if ((KeyButton1Value>125) and (KeyButton1Value<135))), значение с кнопки должно попадать в диапазон от 125 до 135. При опорном напряжении 5В единица равна примерно 0.005В. Т.е. напряжение на этой кнопке, например, может гулять от 0.61В до 0.66В примерно. При желании можно без проблем расширить диапазон в пару раз если не предполагается делать 50 кнопок.
0
Meklon ,  
Дело в том, что у меня не кнопки. У меня прецизионное измерение сопротивления с замером времени разрядки конденсатора. Но мне для вычисления реального сопротивления критично знать уровни зарядки его и опорное напряжение, с которым идёт сравнение. Собственно и стоит вопрос как можно максимально точно стабилизировать эти значения. Вопрос экономии на спичках не стоит — установка штучная для экспериментальных целей.
0
Alexeyslav ,  
Влияние вольт-фарадной характеристики конденсатора на точность измерения учтено? Может, все-таки воспользоваться образцовым резистором, и как-то построить эксперимент вокруг относительных измерений… либо взять любой резистор и измерить его на высокоточном приборе, и эту константу использовать в расчетах.
Но ИМХО, АЦП встроенный в МК тут в принципе не подойдет, у него заявлена нелинейность в пределах 0.5% — с такой же точностью получится измерить опорное напряжение, а смысл тогда?
0
Meklon ,  
Я АЦП не использую. У меня таймер времени разрядки останавливается по прерыванию, которое активируется при падении заряда конденсатора до опорного уровня. Сейчас абстрактно с 5.14 до 3.3 Вольт.
Повторяемость составила 0.14% на нескольких тысячах измерений стандартного резистора. На рисунке по оси Y — микросекунды. И то виден шум от колебаний напряжения. Если его убрать — точность еще раз в 5 увеличится.
0
kumbr_87 ,   * (был изменён)
Можно купить микросхему-точный источник опорного напряжения, их тысячи всяких разных есть


Но только эта мера вряд ли поможет, особенно при использовании mega 2560. Во всяком случае стаб на 3.3В у нее плохой. Практически все наверное кто сталкивался с использованием передатчиков nrf24l01 на это ругались, на Mega он не работает а на uno работает. Решением было параллельно стабу припаять кондер на тысячу другую микрофарад.
0
Alexeyslav ,  
Точность от этого врятли увеличится, цена деления шкалы — возможно, и воспроизводимость.
0
Meklon ,   * (был изменён)
Для меня допустимо работать в условных единицах сопротивления — микросекундах разрядки. Но на будущее очень хочется механизм пересчета. Аналоговый компаратор вроде весьма точен. Нужно лишь пересчитать. А для этого нужны прецизионные источники напряжений. Я хотел использовать 2 — на 5 вольт, чтобы питать всю плату через +5V без использования родного стабилизатора и на 3.3 вольта как опорное для аналогового компаратора. Вопрос выбора микросхем.
+2
Potok ,  
У АЦП есть такой вход, называется «опорное напряжение». Это есть, так сказать, верхний предел измерения. То есть, АЦП нам показывает, как относится измеряемое напряжение к опорному, а не его абсолютную величину. И как я понимаю, у автора опорка не формируется отдельным источником, а просто соединена с плюсом питания. Таким образом, даже если схему не от +5, а от +3.3 питать, то она будет работать — да, на выходе делителя будет не +2.5, а +1.65, к примеру, но и опорное напряжение не +5, а +3.3! И АЦП в обоих случаях скажем нам 512.
0
Alexeyslav ,  
У АЦП есть много режимов работы, основной режим работы — это когда шкала АЦП привязана к напряжению питания и будь там хоть 3 вольта — 1023 он покажет когда на вход подашь напряжение равное напряжению питания. Вот когда надо будет измерять напряжение, тогда будут проблемы… но ведь никто не запрещает оперативно переключать источник опорного напряжения! Правда, при переключении надо выдерживать паузу перед первым измерением чтобы все переходные процессы прошли.
0
+1 –1
BubaVV ,  
Такая клавиатура начнет очень сильно глючить, когда кнопки выработают свой ресурс, окислятся и забьются грязью. У кнопки в замкнутом состоянии будет сопротивление килоом 20. Сталкивался с таким на практике, судя по форумам случай достаточно типичный
0
+1 –1
kumbr_87 ,  
Довольно странно что вы столкнулись с такой проблемой так как сопротивление кнопки никак не должно влиять на потенциал. Для примера две схемы:

верхняя в начале работы, нижняя после старения кнопок. Для каждой кнопки после старения будет свое значение R11, но в данном случае R11 будет работать как токоограничивающий резистор а не как делитель напряжения.
Подозреваю что проблемы могут возникнуть если использовать слишком большие значения R6-R10. Тогда ток со временем окисления может опуститься до уровня погрешности и АЦП не сможет стабильно работать.
0
UnknownType ,   * (был изменён)
del
0
BubaVV ,  
Возможно, схема была реализована чуть по другом, но с той же идеей «гирлянда кнопок и резисторов на одной или двух ногах МК»
0
FisHlaBsoMAN ,   * (был изменён)
У меня кнопки реализованы через диоды и опрос всех пинов. Но у меня их 54(56). Можно хоть всё нажать одновременно, только необходимо опрашивать их постоянно. Но у нас несколько разные задачи — у меня возможность нажать хоть все сразу.

Код для матрицы на 56 кнопок типа такой:
картинка
image


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

код
#define F_CPU 16000000UL // 16 MHz
#include <util/delay.h>

const bool debugOnly = true;

const byte offset = 36 + 12; //i forgot 34-2 or 38-2. I did a brute force =)

byte keyMap[7];//button matrix
byte oldKeyMap[7];//tmp/old button matrix to be compared with the old value.

const byte vOutPins[] = {13, 15, 16, 17, 11, 12, 2, 3}; //output pins
const byte vInPins[] = {4, 5, 6, 7, 8, 9, 10}; //input/receive pins

int btnNmbr = 0; //init button iterator

void setup() {

        if (debugOnly)   Serial.begin(31250);

	//make zero array
	for (byte i = 0; i <= 7; i++) {
		for (byte j = 0; j <= 6; j++) {
			bitWrite(keyMap[j], i, false);
			//keyMap[j]=255;
		}
	}
	//setup out pins
	for (byte i = 0; i <= 7; i++) {
		pinMode(vOutPins[i], OUTPUT);
	}

	//setip in/receive pins
	for (byte i = 0; i <= 6; i++) {
		pinMode(vInPins[i], INPUT);
	}
}

void loop() {

	for (byte i = 0; i <= 7; i++) {

		digitalWrite(vOutPins[i], HIGH); //set HIGH level in i' block

		for (byte j = 0; j <= 6; j++) {

			if (digitalRead(vInPins[j])) { //check HIGH level in j button on i block
				bitWrite(keyMap[j], i, 1);
			} else {
				bitWrite(keyMap[j], i, 0);
			}
		}//end for j

		digitalWrite(vOutPins[i], LOW); //set LOW level in i' block
		_delay_us(100);//that the vOutPin had time go off
	}//end for i

	//buttons array looks:
	//  1  5  9 13
	//  2  6 10 14
	//  3  7 11 15
	//  4  8 12 16

	btnNmbr = 56;//init button number. i connect buttons reverse

	int m = 0;
	for (byte j = 0; j <= 6; j++) { //check buttons by columns
        Serial.println(keyMap[j], BIN);
		//Serial.println(keyMap[j],BIN);
		for (byte i = 0; i <= 7; i++) {
			btnNmbr--;
			if (bitRead(keyMap[j], i) != bitRead(oldKeyMap[j], i)) {
				bitWrite(oldKeyMap[j], i, bitRead(keyMap[j], i));
				if (!debugOnly)  {

					//do smth
				}
			}

			if (debugOnly && m > 7) {
				if (debugOnly)Serial.print(" | ");
				m = 0;
			}

			if (debugOnly) {
				m++;
				if (bitRead(keyMap[j], i) == 0) {
					Serial.print("-");
				} else {
					Serial.print("#");
				}
			}

		}//end for i
	}//end for j
	if (debugOnly) Serial.println("");
	Serial.println(freeRam());
}

0
Int_13h ,  
С диодами явно перебор. Хватило бы только по числу линий Дата, если в один момент времени выбран только один Селект.
0
FisHlaBsoMAN ,  
Не хватило бы. Если я вас правильно понял, то в одном блоке кнопки одновременно нажать бы не получилось.
0
Alexeyslav ,  
Проблему одновременных нажатий решают диоды, по количеству кнопок.
0
UnknownType ,  
Что-то не могу сообразить, к каким глюкам приведет преобразование типов (unsigned long) micros()- (long) KeyButton1TimePress > (long) 25000 при переполнении счетчиков микросекунд на 5-е и 9-е сутки. Типа нажали кнопку, подождали 5 суток нажали её же.
0
kumbr_87 ,  
Micros() выдает микросекунды так что обновляется чаще — раз в 70 минут примерно. Раньше не задумывался над этим и довольно долго клавиатурой пользовался без перезагрузки, явно больше 70 минут, никаких проблем не замечал. Стало интересно и провел
эксперимент:
long A;
unsigned long B;
void setup() {
Serial.begin(115200);
}
void loop() {
A=2000000000;
Serial.println(A);
A=3000000000;
B=4000000000;
Serial.println(A);
Serial.println(B-A);
delay(10000);
}

Получилось следующее:
2000000000 (первый serial.print, все как и должно быть)
-1294967296 (второй serial.print,, достигли знакового разряда, знак поменялся, все как и должно быть)
1000000000 (третий serial.print)

Т.е. до достижения верхнего предела для unsigned long никаких проблем не должно быть. Но если зажать кнопку перед самым обнулением счетчика micros() то да, будет небольшая аномалия в поведении программы:
1. Будет пропущена одна обработка нажатия кнопки в цикле loop.
2. В процедуре KeyBoardCalculate() результат строки
KeyButton1TimeFromPress=micros()-KeyButton1TimePress;

будет отрицательным т.к. KeyButton1TimePress будет заведомо больше чем micros(). Это приведет к тому что условия для первого и повторного нажатия:
if ( ((KeyButton1TimeFromPress)>KeyButton1Latency) and (KeyButton1WasChecked==0))
if ( ((KeyButton1TimeFromPress)>(KeyButton1RepeatLatency+KeyButton1RepeatTimePress)) and (KeyButton1WasChecked==1))

не будут выполняться и соответственно клавиатура временно не будет работать пока мы не отпустим кнопку и тем самым не обновим переменную KeyButton1TimePress
KeyButton1TimePress=micros(); 

В целом думаю проблема решится если добавить в процедуру KeyBoardCalculate() условие:
if(KeyButton1TimePress>micros()) {KeyButton1TimePress=0;}

0
UnknownType ,   * (был изменён)
Просто замените в статье тип всем переменным (и константам), связанным со временем c long на unsigned long и упомянутая дырка должна исчезнуть без добавления дополнительных условий.

P.S. лажанулся в 1000раз, перепутав micros и millis, уж извините.
0
kumbr_87 ,  
Я допустил ошибку. Строка:
KeyButton1TimeFromPress=micros()-KeyButton1TimePress; 

не примет отрицательное значение т. к. типы будет приводиться к типу micros() т. е. к без знаковым в результате когда micros() обнулится результатом этой строки будет значение равное максимальному значению unsigned long — KeyButton1TimePress;
т. е. значение продолжит возрастать с учетом разницы на момент обнуления. Например если мы сохранили в переменную KeyButton1TimePress значение 4294907295 (на 60000 меньше максимального)
то когда micros() сбросится и будет принимать значения 2,3,4,5… результатом строки будет 60003,60004,60005 и т.д… В итоге обнуление micros() никак не повлияет на работу программы, все условия будут выполняться и все будет хорошо. Конечно за исключением момента когда мы нажмем кнопку и будем держать ее 70 минут пока цикл не пойдет по кругу. Этот случай я не рассматривал хотя скорей всего выражение KeyButton1RepeatLatency+KeyButton1RepeatTimePress в условии
if ( ((KeyButton1TimeFromPress)>(KeyButton1RepeatLatency+KeyButton1RepeatTimePress)) and (KeyButton1WasChecked==1))

будет также идти по кругу и в итоге проблем не будет.
0
+1 –1
madprogrammer ,  
Автор заново изобрел m2tklib?
0
FisHlaBsoMAN ,   * (был изменён)
У меня тоже есть прототип меню, пока правда с ним временно завязал в связи с завалом по работе и невероятной силы лени, да и с AVR перехожу на ARM (текущие мк по работе STM32L152, STM32F0). Оно пока в очень сыром виде, по моему на компе валяется версия чуть новее. Тут многое является говнокодом и хардкодом. Хотел статью о нём написать, но дописать не могу. Интересно было написать что то не очень требовательное к ресурсам, а то пытался подружить одно меню с одним очень жирным проектом (о котором пока статью еще не начал писать), так памяти не хватает даже после многочисленных оптимизаций, возможно что то можно написать иначе. Может кому понадобится и будет полезно bitbucket.org/fishlabsoman/menuarduino/src/. Код тестировался на симуляторе поэтому управление из серийника.
//скролл это боль…